diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml new file mode 100644 index 0000000..47a0f28 --- /dev/null +++ b/.github/workflows/npm-publish.yml @@ -0,0 +1,39 @@ +name: Publish +on: + release: + types: [created] +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: 16 + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir) + - uses: actions/cache@v3 + id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + - run: yarn install + - run: yarn compile + - run: yarn lint-ts + + publish-npm: + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 16 + registry-url: https://registry.npmjs.org/ + - run: npm ci + - run: npm publish + env: + NODE_AUTH_TOKEN: ${{secrets.GH_NPM_TOKEN}} diff --git a/.github/workflows/slither.yaml b/.github/workflows/slither.yaml index ecc6661..63a523d 100644 --- a/.github/workflows/slither.yaml +++ b/.github/workflows/slither.yaml @@ -4,7 +4,7 @@ on: branches: - master - release-v* - + - main jobs: analyze: runs-on: ubuntu-latest diff --git a/.github/workflows/test-fork.yaml b/.github/workflows/test-fork.yaml index dff727c..d0c43a2 100644 --- a/.github/workflows/test-fork.yaml +++ b/.github/workflows/test-fork.yaml @@ -1,8 +1,8 @@ on: ["push"] name: Test-Forks - jobs: build: + if: github.event.pull_request.draft == false runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/test-vaults.yaml b/.github/workflows/test-vaults.yaml index 7b276da..e9401ec 100644 --- a/.github/workflows/test-vaults.yaml +++ b/.github/workflows/test-vaults.yaml @@ -2,6 +2,7 @@ on: ["push"] name: Test-Vaults jobs: build: + if: github.event.pull_request.draft == false runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 diff --git a/.gitignore b/.gitignore index 57d1a33..d9fac20 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,8 @@ artifacts # Typechain /types/generated +docs/natspec + # VS Code Solidity Extension bin/contracts diff --git a/.npmrc b/.npmrc index 8cc771f..c2d68a3 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,2 @@ -@mstable:registry=https://npm.pkg.github.com +# Github Package Registry +# @mstable:registry=https://npm.pkg.github.com diff --git a/.solcover.js b/.solcover.js index d01ee25..73f4e15 100644 --- a/.solcover.js +++ b/.solcover.js @@ -2,6 +2,7 @@ module.exports = { skipFiles: [ "integrations", "upgradability", + "contracts/interfaces", "./vault/liquidity/convex/Convex3CrvBasicVault.sol", "./shared/SafeCastExtended.sol", "z_mocks", diff --git a/README.md b/README.md index b3db7ea..b16266a 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ Key folders: ## Testing -Tests are written with [Hardhat](https://hardhat.org/), [Ethers.js](https://docs.ethers.io), [Waffle](https://ethereum-waffle.readthedocs.io/) & [Typescript](https://www.typescriptlang.org/), using [Typechain](https://github.com/dethcrypto/TypeChain) to generate typings for all contracts. Tests are executed using `hardhat` in hardhats evm. +Tests are written with [Hardhat](https://hardhat.org/), [Ethers.js](https://docs.ethers.io), [Mocha](https://mochajs.org/) & [Typescript](https://www.typescriptlang.org/), using [Typechain](https://github.com/dethcrypto/TypeChain) to generate typings for all contracts. Tests are executed using `hardhat` in hardhats evm. ``` $ yarn test @@ -84,7 +84,7 @@ $ yarn test [Solidity-coverage](https://github.com/sc-forks/solidity-coverage) is used to run coverage analysis on test suite. -This produces reports that are visible in the `/coverage` folder, and navigatable/uploadable. Ultimately they are used as a reference that there is some sort of adequate cover, although they will not be a source of truth for a robust test framework. Reports publically available on [coveralls](https://coveralls.io/github/mstable/metavaults). +This produces reports that are visible in the `/coverage` folder, and navigatable/uploadable. Ultimately they are used as a reference that there is some sort of adequate cover, although they will not be a source of truth for a robust test framework. Reports are publicly available on [coveralls](https://coveralls.io/github/mstable/metavaults). _NB: solidity-coverage runs with solc `optimizer=false` (see [discussion](https://github.com/sc-forks/solidity-coverage/issues/417))_ @@ -177,6 +177,18 @@ export NODE_URL=https://mainnet.infura.io/v3/yourApiKey yarn task token-transfer --network mainnet --asset MTA --recipient mStableDAO -- amount 1000 ``` +## Document generation from Natspec + +The contract Natspec can be generated into a markdown file in the `docs/natspec` folder using the following command. + +``` +yarn docgen +``` + +The markdown for the relevant contracts can then be copied into GitBook. + +Unfortunately the generated markdown will not include inherited classes. These need to be manually include for now. + ## Other mStable Meta Vault repositories - https://github.com/mstable/mStable-defender diff --git a/SecondMetaVaultAudit.md b/SecondMetaVaultAudit.md deleted file mode 100644 index a95041e..0000000 --- a/SecondMetaVaultAudit.md +++ /dev/null @@ -1,82 +0,0 @@ -# Second Meta Vaults Security Audit - -Scope of the security audit of [mStable](https://mstable.org/)'s new Meta Vaults, October 2022. - -# Logic - -See [3Crv Convex Vaults](./3CrvConvexVaults.md) for an explanation of what the different vaults do and how value flows between them. - -# Code - -Only the high risk contracts will be audited. Simpler contracts that have been previously audited are out of scope given the tight timeframes. - -All code is in the [metavaults](https://github.com/mstable/metavaults) private repository with tag [v0.0.3](https://github.com/mstable/metavaults/tree/v0.0.3). - -# Contract scope - -All contract are under the [contracts](./contracts/) folder. - -## In scope - -- [peripheral](./contracts/peripheral/) - - [Cowswap](./contracts/peripheral/Cowswap) the [CowSwapSeller](./contracts/peripheral/Cowswap/CowSwapSeller.sol) contract that integrates with CoW Swap. -- [shares](./contracts/shared/) just the [SingleSlotMapper](./contracts/shared/SingleSlotMapper.sol) contract as it has not been audited. The others have been through multiple audits. -- [vault](./contracts/vault) all vault contract except the ones listed below in the out of scope section. - - [swap](./contracts/vault/swap/) only [CowSwapDex](./contracts/vault/swap/CowSwapDex.sol) is in scope. - -## Out of scope - -Any contracts in the following are out of scope as they have previously been audited or are just used for testing. - -- [governance](./contracts/governance) all out of scope as perviously audited. -- [interfaces](./contracts/interfaces) contract interfaces. -- [nexus](./contracts/nexus) all out of scope as perviously audited. -- [peripheral](./contracts/peripheral/) - - [Convex](./contracts/peripheral/Convex) are just interfaces. - - [Curve](./contracts/peripheral/Curve) are interfaces, libraries or testing contracts. The libraries are ports of Curve's Vyper code with gas optimizations. They are stateless so are not a high risk of containing security issues. Given their logic complexity and they have previously been audited, they are out of scope of this audit. - - [OneInch](./contracts/peripheral/OneInch) are just interfaces and will not be initially used. -- [token](./contracts/tokens) all out scope as perviously audited. -- [upgradability](./contracts/upgradability) all out of scope as perviously audited. -- [vault](./contracts/vault) any BasicVaults are out of scope as they are just used for testing. Specifically, `PeriodicAllocationBasicVault`, `SameAssetUnderlyingsBasicVault`, `PerfFeeBasicVault`, `LiquidatorBasicVault`, `LiquidatorStreamBasicVault`, `LiquidatorStreamFeeBasicVault`, `Convex3CrvBasicVault`, `Curve3CrvBasicMetaVault` and `BasicSlippage` are all out of scope. - - [swap](./contracts/vault/swap/) contracts [BasicDexSwap](./contracts/vault/swap/BasicDexSwap.sol) and [OneInchDexSwap](./contracts/vault/swap/OneInchDexSwap.sol) are out of scope. -- [z_mocks](./contracts/z_mocks/) are just used for unit testing. -- [BasicVault](./contracts/vault/BasicVault.sol) and [LightBasicVault](./contracts/vault/LightBasicVault.sol) are out of scope. - -# Mainnet Contracts - -| Contract | Address| -|---|---| -|CowSwapDex | [0x8E9A9a122F402CD98727128BaF3dCCAF05189B67](https://etherscan.io/address/0x8E9A9a122F402CD98727128BaF3dCCAF05189B67) | -| Liquidator Impl | [0x56c358d4E8f9b678fc24a8Cc4aA02c02A1393fAD](https://etherscan.io/address/0x56c358d4E8f9b678fc24a8Cc4aA02c02A1393fAD) | -| Liquidator Proxy | [0xD298291059aed77686037aEfFCf497A321A4569e](https://etherscan.io/address/0xD298291059aed77686037aEfFCf497A321A4569e) | -| Curve3CrvMetapoolCalculatorLibrary | [0x5de8865522A61FC9bf2A3ca1A7D196A42863Ea56](https://etherscan.io/address/0x5de8865522A61FC9bf2A3ca1A7D196A42863Ea56) | -| Curve3CrvFactoryMetapoolCalculatorLibrary | [0x3206bf36B1e1764B4C40c5A51A8E237DC4cB10a9](https://etherscan.io/address/0x3206bf36B1e1764B4C40c5A51A8E237DC4cB10a9) | -| Curve3CrvCalculatorLibrary | [0x092C1b41163c85054F008A486BA72347B919aFa7](https://etherscan.io/address/0x092C1b41163c85054F008A486BA72347B919aFa7) | -| FRAX Convex Vault impl | [0x6DE3703418A075481c7ce01199B8e8F82C129485](https://etherscan.io/address/0x6DE3703418A075481c7ce01199B8e8F82C129485) | -| FRAX Convex Vault proxy | [0x98c5910823C2E67d54e4e0C03de44043DbfA7ca8](https://etherscan.io/address/0x98c5910823C2E67d54e4e0C03de44043DbfA7ca8) | -| mUSD Convex Vault impl | [0xa79e8e15dfd58cd5a93ed3f00bbbbe303f2a0cd8](https://etherscan.io/address/0xa79e8e15dfd58cd5a93ed3f00bbbbe303f2a0cd8) | -| mUSD Convex Vault proxy | [0xB9B47E72819934d7A5d60Bf08cD2C78072383EBb](https://etherscan.io/address/0xB9B47E72819934d7A5d60Bf08cD2C78072383EBb) | -| BUSD Convex Vault impl | [0xCd619AADd1DD2e423D1f3C725a25296c7a74281a](https://etherscan.io/address/0xCd619AADd1DD2e423D1f3C725a25296c7a74281a) | -| BUSD Convex Vault proxy | [0x87Ed92648fAE3b3930577c92c8A247b127ED8949](https://etherscan.io/address/0x87Ed92648fAE3b3930577c92c8A247b127ED8949) | -| 3Crv Meta Vault impl | [0xe3CEab97Fb4289f3A4C979E74D20c90Ab16e1F7d](https://etherscan.io/address/0xe3CEab97Fb4289f3A4C979E74D20c90Ab16e1F7d) | -| 3Crv Meta Vault proxy | [0x9614a4C61E45575b56c7e0251f63DCDe797d93C5](https://etherscan.io/address/0x9614a4C61E45575b56c7e0251f63DCDe797d93C5) | -| USDC 3CRV Convex Meta Vault impl | [0x6d68F5b8c22A549334ca85960978f9dE4DebA2D3](https://etherscan.io/address/0x6d68F5b8c22A549334ca85960978f9dE4DebA2D3) | -| USDC 3CRV Convex Meta Vault proxy | [0x455fB969dC06c4Aa77e7db3f0686CC05164436d2](https://etherscan.io/address/0x455fB969dC06c4Aa77e7db3f0686CC05164436d2) | - - -# Third Party Dependencies - -## Contract Libraries - -- [OpenZeppelin](https://www.openzeppelin.com/contracts) is used for ERC20 tokens, access control, initialization, reentry protection, proxies, casting and math operations. - -## Protocols - -- [Curve Finance](https://curve.fi/) used to generate yield on stablecoin deposits. -- [Convex Finance](https://www.convexfinance.com/) used to enhance the yield from Curve pools. -- [Cowswap](https://cowswap.exchange/) used for swapping Convex reward tokens (CRV and CVX) to DAI, USDC or USDT. - -## Standards - -- [EIP-20](https://eips.ethereum.org/EIPS/eip-20) -- [EIP-4626](https://eips.ethereum.org/EIPS/eip-4626) diff --git a/contracts/interfaces/ILiquidatorV2.sol b/contracts/interfaces/ILiquidatorV2.sol new file mode 100644 index 0000000..75b5ff5 --- /dev/null +++ b/contracts/interfaces/ILiquidatorV2.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity 0.8.17; + +/** + * @title Collects reward tokens from vaults, swaps them and donated the purchased token back to the vaults. + * Supports asynchronous and synchronous swaps. + * @author mStable + * @dev VERSION: 1.0 + * DATE: 2022-05-11 + */ +interface ILiquidatorV2 { + function collectRewards(address[] memory vaults) + external + returns ( + address[][] memory rewardTokens, + uint256[][] memory rewards, + address[][] memory purchaseTokens + ); + + function initiateSwap( + address rewardToken, + address assetToken, + bytes memory data + ) external returns (uint256 batch, uint256 rewards); + + function swap( + address rewardToken, + address assetToken, + uint256 minAssets, + bytes memory data + ) + external + returns ( + uint256 batch, + uint256 rewards, + uint256 assets + ); + + function initiateSwaps( + address[] memory rewardTokens, + address[] memory assetTokens, + bytes[] memory datas + ) external returns (uint256[] memory batchs, uint256[] memory rewards); + + function settleSwaps( + address[] memory rewardTokens, + address[] memory assetTokens, + uint256[] memory assets, + bytes[] memory datas + ) external returns (uint256[] memory batchs, uint256[] memory rewards); +} diff --git a/contracts/peripheral/Curve/Curve3CrvFactoryMetapoolCalculator.sol b/contracts/peripheral/Curve/Curve3CrvFactoryMetapoolCalculator.sol deleted file mode 100644 index 86eaf91..0000000 --- a/contracts/peripheral/Curve/Curve3CrvFactoryMetapoolCalculator.sol +++ /dev/null @@ -1,195 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.17; - -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -import { Curve3CrvFactoryMetapoolCalculatorLibrary } from "./Curve3CrvFactoryMetapoolCalculatorLibrary.sol"; - -/** - * @title `Curve3CrvFactoryMetapoolCalculatorLibrary` wrapper for testing. - * @notice This has been configured to work for factory metapools with only two coins, 18 decimal places and 3Pool (3Crv) as the base pool. - * @author mStable - * @dev VERSION: 1.0 - * DATE: 2022-08-26 - */ -contract Curve3CrvFactoryMetapoolCalculator { - /// @notice Curve's 3Crv based metapool. eg the BUSD3CRV-f pool - address public immutable metapool; - /// @notice Curve's Liquidity Provider (LP) token for the metapool. eg BUSD3CRV-f. - address public immutable metapoolToken; - - /** - * @param _metapool Curve metapool. eg the BUSD3CRV-f pool. This is different to the metapool LP token. - * @param _metapoolToken Curve liquidity provider token for the metapool. eg BUSD3CRV-f - */ - constructor(address _metapool, address _metapoolToken) { - metapool = _metapool; - metapoolToken = _metapoolToken; - } - - /** - * @notice Calculates the amount of metapool liquidity provider tokens, eg BUSD3CRV-f, - * to mint for depositing a fixed amount of tokens, eg BUSD or 3Crv, to the metapool. - * @param _tokenAmount The amount of coins, eg BUSD or 3Crv, to deposit to the metapool. - * @param _coinIndex The index of the coin in the metapool. 0 = eg BUSD, 1 = base coin, eg 3Crv. - * @return mintAmount_ The amount of metapool liquidity provider tokens, eg BUSD3CRV-f, to mint. - * @return invariant_ The metapool invariant before the deposit. This is the USD value of the metapool. - * @return totalSupply_ Total metapool liquidity provider tokens, eg BUSD3CRV-f, before the deposit. - * @return baseVirtualPrice_ Virtual price of the base pool in USD and `VIRTUAL_PRICE_SCALE` decimals. - */ - function calcDeposit(uint256 _tokenAmount, uint256 _coinIndex) - public - view - returns ( - uint256 mintAmount_, - uint256 invariant_, - uint256 totalSupply_, - uint256 baseVirtualPrice_ - ) - { - return - Curve3CrvFactoryMetapoolCalculatorLibrary.calcDeposit( - metapool, - metapoolToken, - _tokenAmount, - _coinIndex - ); - } - - /** - * @notice Calculates the amount of metapool liquidity provider tokens, eg BUSD3CRV-f, - * to burn for withdrawing a fixed amount of tokens, eg BUSD or 3Crv, from the metapool. - * @param _tokenAmount The amount of coins, eg BUSD or 3Crv, to withdraw from the metapool. - * @param _coinIndex The index of the coin in the metapool. 0 = eg BUSD, 1 = base coin, eg 3Crv. - * @return burnAmount_ The amount of metapool liquidity provider tokens, eg BUSD3CRV-f, to burn. - * @return invariant_ The metapool invariant before the withdraw. This is the USD value of the metapool. - * @return totalSupply_ Total metapool liquidity provider tokens, eg BUSD3CRV-f, before the withdraw. - */ - function calcWithdraw(uint256 _tokenAmount, uint256 _coinIndex) - public - view - returns ( - uint256 burnAmount_, - uint256 invariant_, - uint256 totalSupply_, - uint256 baseVirtualPrice_ - ) - { - return - Curve3CrvFactoryMetapoolCalculatorLibrary.calcWithdraw( - metapool, - metapoolToken, - _tokenAmount, - _coinIndex - ); - } - - /** - * @notice Calculates the amount of metapool coins, eg BUSD or 3Crv, to deposit into the metapool - * to mint a fixed amount of metapool liquidity provider tokens, eg BUSD3CRV-f. - * @param _mintAmount The amount of metapool liquidity provider token, eg BUSD3CRV-f, to mint. - * @param _coinIndex The index of the coin in the metapool. 0 = eg BUSD, 1 = base coin, eg 3Crv. - * @return tokenAmount_ The amount of coins, eg BUSD or 3Crv, to deposit. - * @return invariant_ The invariant before the mint. This is the USD value of the metapool. - * @return totalSupply_ Total metapool liquidity provider tokens, eg BUSD3CRV-f, before the mint. - */ - function calcMint(uint256 _mintAmount, uint256 _coinIndex) - public - view - returns ( - uint256 tokenAmount_, - uint256 invariant_, - uint256 totalSupply_, - uint256 baseVirtualPrice_ - ) - { - return - Curve3CrvFactoryMetapoolCalculatorLibrary.calcMint( - metapool, - metapoolToken, - _mintAmount, - _coinIndex - ); - } - - /** - * @notice Calculates the amount of metapool coins, eg BUSD or 3Crv, that will be received from the metapool - * from burning a fixed amount of metapool liquidity provider tokens, eg BUSD3CRV-f. - * @param _burnAmount The amount of metapool liquidity provider token, eg BUSD3CRV-f, to burn. - * @param _coinIndex The index of the coin in the metapool. 0 = eg BUSD, 1 = base coin, eg 3Crv. - * @return tokenAmount_ The amount of coins, eg BUSD or 3Crv, to deposit. - * @return invariant_ The invariant before the redeem. This is the USD value of the metapool. - * @return totalSupply_ Total metapool liquidity provider tokens, eg BUSD3CRV-f, before the redeem. - */ - function calcRedeem(uint256 _burnAmount, uint256 _coinIndex) - public - view - returns ( - uint256 tokenAmount_, - uint256 invariant_, - uint256 totalSupply_ - ) - { - return - Curve3CrvFactoryMetapoolCalculatorLibrary.calcRedeem( - metapool, - metapoolToken, - _burnAmount, - _coinIndex - ); - } - - /** - * @notice Gets the USD price of the base pool liquidity provider token scaled to `VIRTUAL_PRICE_SCALE`. eg 3Crv/USD. - * Note the base pool virtual price is different to the metapool virtual price. - * The base pool's virtual price is used to price 3Pool's 3Crv back to USD. - */ - function getBaseVirtualPrice() external view returns (uint256 baseVirtualPrice_) { - baseVirtualPrice_ = Curve3CrvFactoryMetapoolCalculatorLibrary.getBaseVirtualPrice(); - } - - /** - * @notice Gets the metapool and basepool virtual prices. These prices do not change with the balance of the coins in the pools. - * This means the virtual prices can not be manipulated with flash loans or sandwich attacks. - * @return metaVirtualPrice_ Metapool's liquidity provider token price in USD scaled to `VIRTUAL_PRICE_SCALE`. eg BUSD3CRV-f/USD - * @return baseVirtualPrice_ Basepool's liquidity provider token price in USD scaled to `VIRTUAL_PRICE_SCALE`. eg 3Crv/USD - */ - function getVirtualPrices() - public - view - returns (uint256 metaVirtualPrice_, uint256 baseVirtualPrice_) - { - (metaVirtualPrice_, baseVirtualPrice_) = Curve3CrvFactoryMetapoolCalculatorLibrary - .getVirtualPrices(metapool, metapoolToken); - } - - /** - * @notice Values metapool liquidity provider (LP) tokens as base pool LP tokens. - * Base pool LP = metapool LP tokens * metapool USD value * base pool virtual price scale / - * (total metapool LP supply * base pool virutal price) - * @param metaLp Amount of metapool liquidity provider tokens to value. - * @return baseLp_ Value in base pool liquidity provider tokens. - */ - function convertToBaseLp(uint256 metaLp) public view returns (uint256 baseLp_) { - baseLp_ = Curve3CrvFactoryMetapoolCalculatorLibrary.convertToBaseLp( - metapool, - metapoolToken, - metaLp - ); - } - - /** - * @notice Values base pool liquidity provider (LP) tokens as metapool LP tokens. - * Metapool LP = base pool LP tokens * base pool virutal price * total metapool LP supply / - * (metapool USD value * base pool virtual price scale) - * @param baseLp Amount of base pool liquidity provider tokens to value. - * @return metaLp_ Value in metapool liquidity provider tokens. - */ - function convertToMetaLp(uint256 baseLp) public view returns (uint256 metaLp_) { - metaLp_ = Curve3CrvFactoryMetapoolCalculatorLibrary.convertToMetaLp( - metapool, - metapoolToken, - baseLp - ); - } -} diff --git a/contracts/peripheral/Curve/Curve3CrvMetapoolCalculator.sol b/contracts/peripheral/Curve/Curve3CrvMetapoolCalculator.sol deleted file mode 100644 index a823b8d..0000000 --- a/contracts/peripheral/Curve/Curve3CrvMetapoolCalculator.sol +++ /dev/null @@ -1,203 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.17; - -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -import { Curve3CrvMetapoolCalculatorLibrary } from "./Curve3CrvMetapoolCalculatorLibrary.sol"; - -/** - * @title Curve3CrvMetapoolCalculatorLibrary wrapper for testing. - * @notice This has been configured to work for metapools with only two coins, 18 decimal places and 3Pool (3Crv) as the base pool. - * @author mStable - * @dev VERSION: 1.0 - * DATE: 2022-07-20 - */ -contract Curve3CrvMetapoolCalculator { - /// @notice Curve's 3Crv based metapool. eg the musd3Crv pool - address public immutable metapool; - /// @notice Curve's Liquidity Provider (LP) token for the metapool. eg musd3Crv. - address public immutable metapoolToken; - - /** - * @param _metapool Curve metapool. eg the musd3Crv pool. This is different to the metapool LP token. - * @param _metapoolToken Curve liquidity provider token for the metapool. eg musd3Crv - */ - constructor( - address _metapool, - address _metapoolToken - ) { - metapool = _metapool; - metapoolToken = _metapoolToken; - } - - /** - * @notice Calculates the amount of metapool liquidity provider tokens, eg musd3Crv, - * to mint for depositing a fixed amount of tokens, eg mUSD or 3Crv, to the metapool. - * @param _tokenAmount The amount of coins, eg mUSD or 3Crv, to deposit to the metapool. - * @param _coinIndex The index of the coin in the metapool. 0 = eg musd, 1 = base coin, eg 3Crv. - * @return mintAmount_ The amount of metapool liquidity provider tokens, eg musd3Crv, to mint. - * @return invariant_ The metapool invariant before the deposit. This is the USD value of the metapool. - * @return totalSupply_ Total metapool liquidity provider tokens, eg musd3Crv, before the deposit. - * @return baseVirtualPrice_ Virtual price of the base pool in USD and `VIRTUAL_PRICE_SCALE` decimals. - */ - function calcDeposit( - uint256 _tokenAmount, - uint256 _coinIndex - ) - public - view - returns ( - uint256 mintAmount_, - uint256 invariant_, - uint256 totalSupply_, - uint256 baseVirtualPrice_ - ) - { - return - Curve3CrvMetapoolCalculatorLibrary.calcDeposit( - metapool, - metapoolToken, - _tokenAmount, - _coinIndex - ); - } - - /** - * @notice Calculates the amount of metapool liquidity provider tokens, eg musd3Crv, - * to burn for withdrawing a fixed amount of tokens, eg mUSD or 3Crv, from the metapool. - * @param _tokenAmount The amount of coins, eg mUSD or 3Crv, to withdraw from the metapool. - * @param _coinIndex The index of the coin in the metapool. 0 = eg musd, 1 = base coin, eg 3Crv. - * @return burnAmount_ The amount of metapool liquidity provider tokens, eg musd3Crv, to burn. - * @return invariant_ The metapool invariant before the withdraw. This is the USD value of the metapool. - * @return totalSupply_ Total metapool liquidity provider tokens, eg musd3Crv, before the withdraw. - */ - function calcWithdraw(uint256 _tokenAmount, uint256 _coinIndex) - public - view - returns ( - uint256 burnAmount_, - uint256 invariant_, - uint256 totalSupply_, - uint256 baseVirtualPrice_ - ) - { - return - Curve3CrvMetapoolCalculatorLibrary.calcWithdraw( - metapool, - metapoolToken, - _tokenAmount, - _coinIndex - ); - } - - /** - * @notice Calculates the amount of metapool coins, eg mUSD or 3Crv, to deposit into the metapool - * to mint a fixed amount of metapool liquidity provider tokens, eg musd3Crv. - * @param _mintAmount The amount of metapool liquidity provider token, eg musd3Crv, to mint. - * @param _coinIndex The index of the coin in the metapool. 0 = eg musd, 1 = base coin, eg 3Crv. - * @return tokenAmount_ The amount of coins, eg mUSD or 3Crv, to deposit. - * @return invariant_ The invariant before the mint. This is the USD value of the metapool. - * @return totalSupply_ Total metapool liquidity provider tokens, eg musd3Crv, before the mint. - */ - function calcMint(uint256 _mintAmount, uint256 _coinIndex) - public - view - returns ( - uint256 tokenAmount_, - uint256 invariant_, - uint256 totalSupply_, - uint256 baseVirtualPrice_ - ) - { - return - Curve3CrvMetapoolCalculatorLibrary.calcMint( - metapool, - metapoolToken, - _mintAmount, - _coinIndex - ); - } - - /** - * @notice Calculates the amount of metapool coins, eg mUSD or 3Crv, that will be received from the metapool - * from burning a fixed amount of metapool liquidity provider tokens, eg musd3Crv. - * @param _burnAmount The amount of metapool liquidity provider token, eg musd3Crv, to burn. - * @param _coinIndex The index of the coin in the metapool. 0 = eg musd, 1 = base coin, eg 3Crv. - * @return tokenAmount_ The amount of coins, eg mUSD or 3Crv, to deposit. - * @return invariant_ The invariant before the redeem. This is the USD value of the metapool. - * @return totalSupply_ Total metapool liquidity provider tokens, eg musd3Crv, before the redeem. - */ - function calcRedeem(uint256 _burnAmount, uint256 _coinIndex) - public - view - returns ( - uint256 tokenAmount_, - uint256 invariant_, - uint256 totalSupply_ - ) - { - return - Curve3CrvMetapoolCalculatorLibrary.calcRedeem( - metapool, - metapoolToken, - _burnAmount, - _coinIndex - ); - } - - /** - * @notice Gets the USD price of one base pool liquidity provider token scaled to `VIRTUAL_PRICE_SCALE`. eg 3Crv/USD. - * This is either going to be from - * 1. The 10 minute cache in the metapool. - * 2. The latest directly from the base pool. - * Note the base pool virtual price is different to the metapool virtual price. - * The base pool's virtual price is used to price 3Pool's 3Crv back to USD. - * @param cached true will try and get the base pool's virtual price from the metapool cache. - * false will get the base pool's virtual price directly from the base pool. - */ - function getBaseVirtualPrice(bool cached) external view returns (uint256 baseVirtualPrice_) { - baseVirtualPrice_ = Curve3CrvMetapoolCalculatorLibrary.getBaseVirtualPrice(metapool, cached); - } - - /** - * @notice Gets the metapool and basepool virtual prices. These prices do not change with the balance of the coins in the pools. - * This means the virtual prices can not be manipulated with flash loans or sandwich attacks. - * @param cached true will try and get the base pool's virtual price from the metapool cache. - * false will get the base pool's virtual price directly from the base pool. - * @return metaVirtualPrice_ Metapool's liquidity provider token price in USD scaled to `VIRTUAL_PRICE_SCALE`. eg musd3Crv/USD - * @return baseVirtualPrice_ Basepool's liquidity provider token price in USD scaled to `VIRTUAL_PRICE_SCALE`. eg 3Crv/USD - */ - function getVirtualPrices(bool cached) - public - view - returns (uint256 metaVirtualPrice_, uint256 baseVirtualPrice_) - { - (metaVirtualPrice_, baseVirtualPrice_) = Curve3CrvMetapoolCalculatorLibrary.getVirtualPrices(metapool, metapoolToken, cached); - } - - /** - * @notice Values metapool liquidity provider (LP) tokens as base pool LP tokens. - * Base pool LP = metapool LP tokens * metapool USD value * base pool virtual price scale / - * (total metapool LP supply * base pool virutal price) - * @param metaLp Amount of metapool liquidity provider tokens to value. - * @param cached true will try and get the base pool's virtual price from the metapool cache. - * false will get the base pool's virtual price directly from the base pool. - * @return baseLp_ Value in base pool liquidity provider tokens. - */ - function convertToBaseLp(uint256 metaLp, bool cached) public view returns (uint256 baseLp_) { - baseLp_ = Curve3CrvMetapoolCalculatorLibrary.convertToBaseLp(metapool, metapoolToken, metaLp, cached); - } - - /** - * @notice Values base pool liquidity provider (LP) tokens as metapool LP tokens. - * Metapool LP = base pool LP tokens * base pool virutal price * total metapool LP supply / - * (metapool USD value * base pool virtual price scale) - * @param baseLp Amount of base pool liquidity provider tokens to value. - * @param cached true will try and get the base pool's virtual price from the metapool cache. - * false will get the base pool's virtual price directly from the base pool. - * @return metaLp_ Value in metapool liquidity provider tokens. - */ - function convertToMetaLp(uint256 baseLp, bool cached) public view returns (uint256 metaLp_) { - metaLp_ = Curve3CrvMetapoolCalculatorLibrary.convertToMetaLp(metapool, metapoolToken, baseLp, cached); - } -} diff --git a/contracts/peripheral/Curve/Curve3PoolCalculator.sol b/contracts/peripheral/Curve/Curve3PoolCalculator.sol deleted file mode 100644 index 3f777fa..0000000 --- a/contracts/peripheral/Curve/Curve3PoolCalculator.sol +++ /dev/null @@ -1,109 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.17; - -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -import { Curve3PoolCalculatorLibrary } from "./Curve3PoolCalculatorLibrary.sol"; -import { ICurve3Pool } from "./ICurve3Pool.sol"; - -/** - * @title Calculates Curve token amounts including fees for the Curve.fi 3Pool. - * @notice This has been configured to work for Curve 3Pool which contains DAI, USDC and USDT. - * @author mStable - * @dev VERSION: 1.0 - * DATE: 2022-07-12 - * @dev See Atul Agarwal's post "Understanding the Curve AMM, Part -1: StableSwap Invariant" - * for an explaination of the maths behind StableSwap. This includes an explation of the - * variables S, D, Ann used in _getD - * https://atulagarwal.dev/posts/curveamm/stableswap/ - */ -contract Curve3PoolCalculator { - /** - * @notice Calculates the amount of liquidity provider tokens (3Crv) to mint for depositing a fixed amount of pool tokens. - * @param _tokenAmount The amount of coins, eg DAI, USDC or USDT, to deposit. - * @param _coinIndex The index of the coin in the pool to withdraw. 0 = DAI, 1 = USDC, 2 = USDT. - * @return mintAmount_ The amount of liquidity provider tokens (3Crv) to mint. - * @return invariant_ The invariant before the deposit. This is the USD value of the pool. - * @return totalSupply_ Total liquidity provider tokens (3Crv) before the deposit. - */ - function calcDeposit(uint256 _tokenAmount, uint256 _coinIndex) - public - view - returns ( - uint256 mintAmount_, - uint256 invariant_, - uint256 totalSupply_ - ) - { - return Curve3PoolCalculatorLibrary.calcDeposit(_tokenAmount, _coinIndex); - } - - /** - * @notice Calculates the amount of liquidity provider tokens (3Crv) to burn for receiving a fixed amount of pool tokens. - * @param _tokenAmount The amount of coins, eg DAI, USDC or USDT, required to receive. - * @param _coinIndex The index of the coin in the pool to withdraw. 0 = DAI, 1 = USDC, 2 = USDT. - * @return burnAmount_ The amount of liquidity provider tokens (3Crv) to burn. - * @return invariant_ The invariant before the withdraw. This is the USD value of the pool. - * @return totalSupply_ Total liquidity provider tokens (3Crv) before the withdraw. - */ - function calcWithdraw(uint256 _tokenAmount, uint256 _coinIndex) - public - view - returns ( - uint256 burnAmount_, - uint256 invariant_, - uint256 totalSupply_ - ) - { - return Curve3PoolCalculatorLibrary.calcWithdraw(_tokenAmount, _coinIndex); - } - - /** - * @notice Calculates the amount of pool coins deposited for minting a fixed amount of liquidity provider tokens (3Crv). - * @param _mintAmount The amount of liquidity provider tokens (3Crv) to mint. - * @param _coinIndex The index of the coin in the pool to withdraw. 0 = DAI, 1 = USDC, 2 = USDT. - * @return tokenAmount_ The amount of coins, eg DAI, USDC or USDT, to deposit. - * @return invariant_ The invariant before the mint. This is the USD value of the pool. - * @return totalSupply_ Total liquidity provider tokens (3Crv) before the mint. - */ - function calcMint(uint256 _mintAmount, uint256 _coinIndex) - public - view - returns ( - uint256 tokenAmount_, - uint256 invariant_, - uint256 totalSupply_ - ) - { - return Curve3PoolCalculatorLibrary.calcMint(_mintAmount, _coinIndex); - } - - /** - * @notice Calculates the amount of pool coins received for redeeming a fixed amount of liquidity provider tokens (3Crv). - * @param _burnAmount The amount of liquidity provider tokens (3Crv) to burn. - * @param _coinIndex The index of the coin in the pool to withdraw. 0 = DAI, 1 = USDC, 2 = USDT. - * @return tokenAmount_ The amount of coins, eg DAI, USDC or USDT, to receive from the redeem. - * @return invariant_ The invariant before the redeem. This is the USD value of the pool. - * @return totalSupply_ Total liquidity provider tokens (3Crv) before the redeem. - */ - function calcRedeem(uint256 _burnAmount, uint256 _coinIndex) - public - view - returns ( - uint256 tokenAmount_, - uint256 invariant_, - uint256 totalSupply_ - ) - { - return Curve3PoolCalculatorLibrary.calcRedeem(_burnAmount, _coinIndex); - } - - /** - * Get 3Pool's virtual price which is in USD. This is the pool's invariant - * divided by the number of LP tokens scaled to `VIRTUAL_PRICE_SCALE` which is 1e18. - * @return virtualPrice_ 3Pool's virtual price in USD scaled to 18 decimal places. - */ - function getVirtualPrice() public view returns (uint256 virtualPrice_) { - return Curve3PoolCalculatorLibrary.getVirtualPrice(); - } -} diff --git a/contracts/peripheral/Curve/README.md b/contracts/peripheral/Curve/README.md index 5a87295..533fcf5 100644 --- a/contracts/peripheral/Curve/README.md +++ b/contracts/peripheral/Curve/README.md @@ -9,12 +9,9 @@ Curve Documentation # Contracts -- [Curve3PoolCalculatorLibrary](./Curve3PoolCalculatorLibrary.sol) Calculates Curve token amounts including fees for the Curve.fi 3Pool. -- [Curve3PoolCalculator](./Curve3PoolCalculator.sol) wraps `Curve3PoolCalculatorLibrary` for testing purposes. -- [Curve3CrvMetapoolCalculatorLibrary](./Curve3CrvMetapoolCalculatorLibrary.sol) Calculates Curve liquidity provider token amounts including fees for 3Crv-based Curve.fi metapools. -- [Curve3CrvMetapoolCalculator](./Curve3CrvMetapoolCalculator.sol) wraps `Curve3CrvMetapoolCalculatorLibrary` for testing purposes. -- [Curve3CrvFactoryMetapoolCalculatorLibrary](./Curve3CrvFactoryMetapoolCalculatorLibrary.sol) Calculates Curve liquidity provider token amounts including fees for 3Crv-based Curve.fi factory metapools. -- [Curve3CrvFactoryMetapoolCalculator](./Curve3CrvFactoryMetapoolCalculator.sol) wraps `Curve3CrvFactoryMetapoolCalculatorLibrary` for testing purposes. +- [Curve3PoolCalculatorLibrary](./Curve3PoolCalculatorLibrary.sol) Library that calculates Curve token amounts including fees for the Curve.fi 3Pool. +- [Curve3CrvMetapoolCalculatorLibrary](./Curve3CrvMetapoolCalculatorLibrary.sol) Library that calculates Curve liquidity provider token amounts including fees for 3Crv-based Curve.fi Metapools. +- [Curve3CrvFactoryMetapoolCalculatorLibrary](./Curve3CrvFactoryMetapoolCalculatorLibrary.sol) Calculates Curve liquidity provider token amounts including fees for 3Crv-based Curve.fi factory Metapools. # Diagrams @@ -26,14 +23,6 @@ Curve Documentation ![Curve Metapool Calculator Library](../../../docs/Curve3CrvMetapoolCalculatorLibrary.svg) -`CurveFraxBpCalculatorLibrary` contract - -![Curve Frax Calculator Library](../../../docs/CurveFraxBpCalculatorLibrary.svg) - -`CurveFraxBpMetapoolCalculatorLibrary` contract - -![Curve Frax Metapool Calculator Library](../../../docs/CurveFraxBpMetapoolCalculatorLibrary.svg) - # Tests Fork tests of the calculation libraries diff --git a/contracts/shared/ImmutableModule.sol b/contracts/shared/ImmutableModule.sol index 4586f73..6fae8dd 100644 --- a/contracts/shared/ImmutableModule.sol +++ b/contracts/shared/ImmutableModule.sol @@ -44,18 +44,6 @@ abstract contract ImmutableModule is ModuleKeys { require(msg.sender == _keeper() || msg.sender == _governor(), "Only keeper or governor"); } - /** - * @dev Modifier to allow function calls only from the Governance. - * Governance is either Governor address or Governance address. - */ - modifier onlyGovernance() { - require( - msg.sender == _governor() || msg.sender == _governance(), - "Only governance can execute" - ); - _; - } - /** * @dev Returns Governor address from the Nexus * @return Address of Governor Contract @@ -64,14 +52,6 @@ abstract contract ImmutableModule is ModuleKeys { return nexus.governor(); } - /** - * @dev Returns Governance Module address from the Nexus - * @return Address of the Governance (Phase 2) - */ - function _governance() internal view returns (address) { - return nexus.getModule(KEY_GOVERNANCE); - } - /** * @dev Return Keeper address from the Nexus. * This account is used for operational transactions that @@ -82,14 +62,6 @@ abstract contract ImmutableModule is ModuleKeys { return nexus.getModule(KEY_KEEPER); } - /** - * @dev Return Liquidator module address from the Nexus - * @return Address of the Liquidator contract - */ - function _liquidator() internal view returns (address) { - return nexus.getModule(KEY_LIQUIDATOR); - } - /** * @dev Return Liquidator V2 module address from the Nexus * @return Address of the Liquidator V2 contract @@ -97,12 +69,4 @@ abstract contract ImmutableModule is ModuleKeys { function _liquidatorV2() internal view returns (address) { return nexus.getModule(KEY_LIQUIDATOR_V2); } - - /** - * @dev Return ProxyAdmin module address from the Nexus - * @return Address of the ProxyAdmin contract - */ - function _proxyAdmin() internal view returns (address) { - return nexus.getModule(KEY_PROXY_ADMIN); - } } diff --git a/contracts/vault/allocate/PeriodicAllocationAbstractVault.sol b/contracts/vault/allocate/PeriodicAllocationAbstractVault.sol index ebc2007..2c9337e 100644 --- a/contracts/vault/allocate/PeriodicAllocationAbstractVault.sol +++ b/contracts/vault/allocate/PeriodicAllocationAbstractVault.sol @@ -389,4 +389,9 @@ abstract contract PeriodicAllocationAbstractVault is function _afterRebalance() internal virtual override { _updateAssetPerShare(); } + + /// @dev Updates assetPerShare after an underlying vault is removed + function _afterRemoveVault() internal virtual override { + _updateAssetPerShare(); + } } diff --git a/contracts/vault/allocate/SameAssetUnderlyingsAbstractVault.sol b/contracts/vault/allocate/SameAssetUnderlyingsAbstractVault.sol index c47a0cd..3813403 100644 --- a/contracts/vault/allocate/SameAssetUnderlyingsAbstractVault.sol +++ b/contracts/vault/allocate/SameAssetUnderlyingsAbstractVault.sol @@ -281,6 +281,9 @@ abstract contract SameAssetUnderlyingsAbstractVault is AbstractVault { // Remove the underlying vault from the vault index map. vaultIndexMap = vaultIndexMapMem.removeValue(underlyingVaultIndex); + // Call _afterRemoveVault + _afterRemoveVault(); + emit RemovedVault(vaultIndex, underlyingVault); } @@ -293,4 +296,10 @@ abstract contract SameAssetUnderlyingsAbstractVault is AbstractVault { * For example, assetsPerShare update after rebalance by PeriodicAllocationAbstractVault */ function _afterRebalance() internal virtual {} + + /** + * @dev Optional hook to do something after an underlying vault is removed. + * For example, assetsPerShare update after removal by PeriodicAllocationAbstractVault + */ + function _afterRemoveVault() internal virtual {} } diff --git a/contracts/vault/fee/PerfFeeAbstractVault.sol b/contracts/vault/fee/PerfFeeAbstractVault.sol index f4a3f98..611cc64 100644 --- a/contracts/vault/fee/PerfFeeAbstractVault.sol +++ b/contracts/vault/fee/PerfFeeAbstractVault.sol @@ -9,8 +9,9 @@ import { VaultManagerRole } from "../../shared/VaultManagerRole.sol"; /** * @notice Abstract ERC-4626 vault that calculates a performance fee since the last time the performance fee was charged. * @author mStable - * @dev VERSION: 1.0 - * DATE: 2022-05-27 + * @dev VERSION: 1.1 + * Created: 2022-05-27 + * Updated: 2022-11-11 * * The following functions have to be implemented * - chargePerformanceFee() @@ -45,16 +46,17 @@ abstract contract PerfFeeAbstractVault is FeeAdminAbstractVault { perfFeesAssetPerShare = PERF_ASSETS_PER_SHARE_SCALE; } - /// @notice Helper function to charge a performance fee in the form of vault shares since the last time a performance fee was charged. - /// As an example, if the assets per share increased by 0.1% in the last week and the performance fee is 4%, the vault shares will be - /// increased by 0.1% * 4% = 0.004% as a fee. If there was 100,000 vault shares, 4 (100,000 * 0.004%) vault shares will be minted as a - /// performance fee. This dilutes the assets per shares of the existing vault shareholders by 0.004%. - /// @dev Created for saving gas by not reading totalSupply() twice. - /// @param currentAssetsPerShare Current assetsPerShare - /// @param totalShares total shares in the vault. - function _chargePerformanceFeeHelper(uint256 currentAssetsPerShare, uint256 totalShares) - internal - { + /** + * @dev charges a performance fee since the last time a fee was charged. + */ + function _chargePerformanceFee() internal { + //Calculate current assets per share. + uint256 totalShares = totalSupply(); + uint256 totalAssets = totalAssets(); + uint256 currentAssetsPerShare = totalShares > 0 + ? (totalAssets * PERF_ASSETS_PER_SHARE_SCALE) / totalShares + : perfFeesAssetPerShare; + // Only charge a performance fee if assets per share has increased. if (currentAssetsPerShare > perfFeesAssetPerShare) { // Calculate the amount of shares to mint as a fee. @@ -71,11 +73,15 @@ abstract contract PerfFeeAbstractVault is FeeAdminAbstractVault { if (feeShares > 0) { _mint(feeReceiver, feeShares); + // Calculate the new assets per share after fee shares have been minted. + // The assets per share has reduced as there are now more shares. + currentAssetsPerShare = (totalAssets * PERF_ASSETS_PER_SHARE_SCALE) / (totalShares + feeShares); + emit PerformanceFee(feeReceiver, feeShares, currentAssetsPerShare); } } - // Store current assets per share. + // Store current assets per share which could be less than the old assets per share. perfFeesAssetPerShare = currentAssetsPerShare; // Hook for implementing contracts to do something after performance fees have been collected. @@ -86,20 +92,13 @@ abstract contract PerfFeeAbstractVault is FeeAdminAbstractVault { } /** - * @notice Charges a performance fee since the last time a fee was charged. - * @dev May need to be called from a trusted account depending on the invest and divest processes. + * @notice Vault Manager charges a performance fee since the last time a fee was charged. + * As an example, if the assets per share increased by 0.1% in the last week and the performance fee is 4%, the vault shares will be + * increased by 0.1% * 4% = 0.004% as a fee. If there was 100,000 vault shares, 4 (100,000 * 0.004%) vault shares will be minted as a + * performance fee. This dilutes the assets per shares of the existing vault shareholders by 0.004%. + * No performance fee is charged if the assets per share drops. + * @dev Called from a trusted account so gains and loses can not be gamed. */ - function _chargePerformanceFee() internal { - //Calculate current assets per share. - uint256 totalShares = totalSupply(); - uint256 currentAssetsPerShare = totalShares > 0 - ? (totalAssets() * PERF_ASSETS_PER_SHARE_SCALE) / totalShares - : perfFeesAssetPerShare; - - //Charge performance fee. - _chargePerformanceFeeHelper(currentAssetsPerShare, totalShares); - } - function chargePerformanceFee() external virtual onlyVaultManager { _chargePerformanceFee(); } diff --git a/contracts/vault/liquidator/Liquidator.sol b/contracts/vault/liquidator/Liquidator.sol index f1fdf8a..273cdc6 100644 --- a/contracts/vault/liquidator/Liquidator.sol +++ b/contracts/vault/liquidator/Liquidator.sol @@ -9,10 +9,10 @@ import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; // Libs import { ILiquidatorVault } from "../../interfaces/ILiquidatorVault.sol"; +import { ILiquidatorV2 } from "../../interfaces/ILiquidatorV2.sol"; +import { DexSwapData, IDexSwap, IDexAsyncSwap } from "../../interfaces/IDexSwap.sol"; import { InitializableReentrancyGuard } from "../../shared/InitializableReentrancyGuard.sol"; - import { ImmutableModule } from "../../shared/ImmutableModule.sol"; -import { DexSwapData, IDexSwap, IDexAsyncSwap } from "../../interfaces/IDexSwap.sol"; struct Liquidation { mapping(address => uint256) vaultRewards; @@ -27,7 +27,7 @@ struct Liquidation { * @dev VERSION: 1.0 * DATE: 2022-05-11 */ -contract Liquidator is Initializable, ImmutableModule, InitializableReentrancyGuard { +contract Liquidator is Initializable, ImmutableModule, InitializableReentrancyGuard, ILiquidatorV2 { using SafeERC20 for IERC20; /// @notice Mapping of reward tokens to asset tokens to a list of liquidation batch data. @@ -57,8 +57,7 @@ contract Liquidator is Initializable, ImmutableModule, InitializableReentrancyGu /** * @param _nexus Address of the Nexus contract that resolves protocol modules and roles. */ - constructor(address _nexus) ImmutableModule(_nexus) { - } + constructor(address _nexus) ImmutableModule(_nexus) {} /** * Initilalise the smart contract with the address of the async and sync swappers. @@ -445,8 +444,8 @@ contract Liquidator is Initializable, ImmutableModule, InitializableReentrancyGu fromAsset: rewardToken, fromAssetAmount: rewards, toAsset: assetToken, - minToAssetAmount: 0, // is not used on async dex - data: data // data(bytes orderUid, bool transfer) for cow swap + minToAssetAmount: 0, // is not used on async dex + data: data // data(bytes orderUid, bool transfer) for cow swap }); // initiates swap on-chain , then off-chain data should monitor when swap is done (fail or success) and call `settleSwap` diff --git a/contracts/vault/liquidator/README.md b/contracts/vault/liquidator/README.md index 1d276db..2baa4bc 100644 --- a/contracts/vault/liquidator/README.md +++ b/contracts/vault/liquidator/README.md @@ -4,7 +4,7 @@ The Liquidator module is responsible for collecting reward tokens from vaults, s The Liquidator's main task is to batch the swapping of rewards collected from multiple vaults. This socializes the gas costs in swapping rewards across multiple vaults. -The Liquidator uses a [Swapper](../swap/README.md) module to do on-chain token swaps. A swapper typically uses a swap aggregator like 1Inch or Coswap but can use decentralized exchanges like Uniswap or Balancer. +The Liquidator uses a [Swapper](../swap/README.md) module to do on-chain token swaps. A swapper typically uses a swap aggregator like 1Inch or CowSwap but can use decentralized exchanges like Uniswap or Balancer. # Contracts diff --git a/contracts/vault/liquidity/AbstractSlippage.sol b/contracts/vault/liquidity/AbstractSlippage.sol index 62db439..2ad4e43 100644 --- a/contracts/vault/liquidity/AbstractSlippage.sol +++ b/contracts/vault/liquidity/AbstractSlippage.sol @@ -66,7 +66,7 @@ abstract contract AbstractSlippage is VaultManagerRole { /// @param _slippage Deposit slippage to apply as basis points i.e. 1% = 100 function _setDepositSlippage(uint256 _slippage) internal { - require(_slippage <= BASIS_SCALE, "Invalid deposit Slippage"); + require(_slippage <= BASIS_SCALE, "Invalid deposit slippage"); depositSlippage = _slippage; emit DepositSlippageChange(msg.sender, _slippage); @@ -74,7 +74,7 @@ abstract contract AbstractSlippage is VaultManagerRole { /// @param _slippage Withdraw slippage to apply as basis points i.e. 1% = 100 function _setWithdrawSlippage(uint256 _slippage) internal { - require(_slippage <= BASIS_SCALE, "Invalid withdraw Slippage"); + require(_slippage <= BASIS_SCALE, "Invalid withdraw slippage"); withdrawSlippage = _slippage; emit WithdrawSlippageChange(msg.sender, _slippage); diff --git a/contracts/vault/liquidity/convex/README.md b/contracts/vault/liquidity/convex/README.md index c275c3e..9a3425d 100644 --- a/contracts/vault/liquidity/convex/README.md +++ b/contracts/vault/liquidity/convex/README.md @@ -21,7 +21,7 @@ Vaults that deposit into a Curve 3Pool (3Crv) based Metapool before depositing t * Invests 3Crv assets in a Curve Metapool and LP token staked in Convex for boosted returns. * Sandwich attack protection on ERC4626 operations `deposit`, `mint`, `withdraw` and `redeem`. * Liquidation of Convex rewards like CRV and CVX for more reinvested 3Crv assets. -* Front running protection against liquidation of rewards by streaming the increase in assets per share. +* Front-running protection against liquidation of rewards by streaming the increase in assets per share. * Fee charged on liquidated Convex rewards. * Vault operations are pausable by the `Governor`. * Emergency asset recovery by the `Governor`. diff --git a/contracts/vault/liquidity/curve/Curve3CrvAbstractMetaVault.sol b/contracts/vault/liquidity/curve/Curve3CrvAbstractMetaVault.sol index b2acf9d..1292423 100644 --- a/contracts/vault/liquidity/curve/Curve3CrvAbstractMetaVault.sol +++ b/contracts/vault/liquidity/curve/Curve3CrvAbstractMetaVault.sol @@ -776,6 +776,9 @@ abstract contract Curve3CrvAbstractMetaVault is AbstractSlippage, LightAbstractV /// @dev Approves Curve's 3Pool contract to transfer assets (DAI, USDC or USDT) from this vault. /// Also approves the underlying Meta Vault to transfer 3Crv from this vault. function _resetAllowances() internal { + _asset.safeApprove(address(Curve3PoolCalculatorLibrary.THREE_POOL), 0); + IERC20(Curve3PoolCalculatorLibrary.LP_TOKEN).safeApprove(address(metaVault), 0); + _asset.safeApprove(address(Curve3PoolCalculatorLibrary.THREE_POOL), type(uint256).max); IERC20(Curve3PoolCalculatorLibrary.LP_TOKEN).safeApprove( address(metaVault), diff --git a/contracts/vault/meta/README.md b/contracts/vault/meta/README.md index b082f6d..5b04494 100644 --- a/contracts/vault/meta/README.md +++ b/contracts/vault/meta/README.md @@ -103,8 +103,11 @@ Note the Vault Manager may be an externally owned account that is controlled by ![Update Assets Per Share](../../../docs/metaVaultUpdateAssetsPerShare.png) -## Rebalance +## Charge Performance fee +Charges a performance fee by looking at the increase in assets per share since the last time a performance fee was charged. The fee is collected in vault shares which dilutes the assets per share of the vault share holders. For example, if the assets per share increased by 0.1% in the last week and the performance fee is 4%, the vault shares will be increased by 0.1% * 4% = 0.004% as a fee. If there was 100,000 vault shares, 4 (100,000 * 0.004%) vault shares will be minted as a performance fee. This dilutes the assets per shares of the existing vault shareholders by 0.004%. + +![Charge Performance fee](../../../docs/metaVaultChargePerformanceFee.png) # Tests diff --git a/contracts/z_mocks/vault/MockLiquidatorMaliciousVault.sol b/contracts/z_mocks/vault/MockLiquidatorMaliciousVault.sol new file mode 100644 index 0000000..7c1f18d --- /dev/null +++ b/contracts/z_mocks/vault/MockLiquidatorMaliciousVault.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity 0.8.17; + +import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import { InitializableToken } from "../../tokens/InitializableToken.sol"; +import { VaultManagerRole } from "../../shared/VaultManagerRole.sol"; +import { AbstractVault } from "../../vault/AbstractVault.sol"; +import { LiquidatorAbstractVault } from "../../vault/liquidator/LiquidatorAbstractVault.sol"; +import { ILiquidatorV2 } from "../../interfaces/ILiquidatorV2.sol"; + +/** + * @notice A simple implementation of the abstract liquidator vault for testing purposes. + * Rewards are added to the vault by simply transferring them to the vault. + * @author mStable + * @dev VERSION: 1.0 + * DATE: 2022-11-30 + */ +contract MockLiquidatorMaliciousVault is AbstractVault, LiquidatorAbstractVault, Initializable { + using SafeERC20 for IERC20; + + /** + * @param _nexus Address of the Nexus contract that resolves protocol modules and roles. + * @param _asset Address of the vault's asset. + */ + constructor(address _nexus, address _asset) AbstractVault(_asset) VaultManagerRole(_nexus) {} + + function initialize( + string calldata _nameArg, + string calldata _symbolArg, + address _vaultManager, + address[] memory _rewardTokens + ) external initializer { + // Set the vault's decimals to the same as the reference asset. + uint8 _decimals = InitializableToken(address(_asset)).decimals(); + InitializableToken._initialize(_nameArg, _symbolArg, _decimals); + + VaultManagerRole._initialize(_vaultManager); + LiquidatorAbstractVault._initialize(_rewardTokens); + } + + function totalAssets() public view override returns (uint256 totalManagedAssets) { + totalManagedAssets = _asset.balanceOf(address(this)); + } + + /** + * @notice Adds tokens to the vault. + * The base implementation only receives vault assets without minting any shares. + * This increases the vault's assets per share. + * @param token The address of the token being donated. + * @param amount The amount of tokens being donated. + */ + function donate(address token, uint256 amount) external override { + require(token == address(_asset), "Donated token not asset"); + _transferAndMint(amount, 0, address(this), true); + } + + /** + * @dev Base implementation returns the vault asset. + * This can be overridden to swap rewards for other tokens. + */ + function _donateToken(address) internal view override returns (address token) { + token = address(_asset); + } + + /** + * @dev Calls liquidator again to collect rewards. + */ + function _beforeCollectRewards() internal virtual override { + address liquidator = _liquidatorV2(); + address[] memory vaults = new address[](1); + vaults[0] = address(this); + // Re-entry call + ILiquidatorV2(liquidator).collectRewards(vaults); + } +} diff --git a/contracts/z_mocks/vault/MockMaliciousDexSwap.sol b/contracts/z_mocks/vault/MockMaliciousDexSwap.sol new file mode 100644 index 0000000..869af77 --- /dev/null +++ b/contracts/z_mocks/vault/MockMaliciousDexSwap.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity 0.8.17; + +// External +import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +// Libs +import { IDexSwap, DexSwapData } from "../../interfaces/IDexSwap.sol"; +import { ImmutableModule } from "../../shared/ImmutableModule.sol"; +import { ILiquidatorV2 } from "../../interfaces/ILiquidatorV2.sol"; + +/** + * @notice Implementation of IDexSwap for testing purposes. + * @author mStable + * @dev VERSION: 1.0 + * DATE: 2022-11-01 + */ +contract MockMaliciousDexSwap is IDexSwap, ImmutableModule, Initializable { + using SafeERC20 for IERC20; + + struct Exchange { + address from; + address to; + uint256 rate; + } + + uint256 public constant RATE_SCALE = 1e18; + + // FromToken => ToToken => Rate + mapping(address => mapping(address => uint256)) public rates; + + event Swapped( + address indexed from, + address indexed to, + uint256 fromAssetAmount, + uint256 toAssetAmount + ); + event RateSet(address indexed from, address indexed to, uint256 rate); + + /** + * @param _nexus Address of the Nexus contract that resolves protocol modules and roles. + */ + constructor(address _nexus) ImmutableModule(_nexus) {} + + function initialize(Exchange[] memory exchanges) external initializer { + uint256 len = exchanges.length; + for (uint256 i = 0; i < len; ) { + _setRate(exchanges[i]); + unchecked { + ++i; + } + } + } + + function swap(DexSwapData memory _swap) external override returns (uint256 toAssetAmount) { + // transfer in the from asset + require( + IERC20(_swap.fromAsset).balanceOf(msg.sender) >= _swap.fromAssetAmount, + "not enough from assets" + ); + IERC20(_swap.fromAsset).safeTransferFrom(msg.sender, address(this), _swap.fromAssetAmount); + // vector attack + address liquidator = _liquidatorV2(); + address[] memory vaults = new address[](1); + vaults[0] = address(this); + + // Re-entry call + ILiquidatorV2(liquidator).swap( + _swap.fromAsset, + _swap.toAsset, + _swap.minToAssetAmount, + _swap.data + ); + + // calculate to asset amount + toAssetAmount = + (_swap.fromAssetAmount * rates[_swap.fromAsset][_swap.toAsset]) / + RATE_SCALE; + + require(toAssetAmount >= _swap.minToAssetAmount, "to asset < min"); + + // transfer out the to asset + require( + IERC20(_swap.toAsset).balanceOf(address(this)) >= toAssetAmount, + "not enough to assets" + ); + + IERC20(_swap.toAsset).safeTransfer(msg.sender, toAssetAmount); + + emit Swapped(_swap.fromAsset, _swap.toAsset, _swap.fromAssetAmount, toAssetAmount); + } + + function setRate(Exchange memory exchange) external onlyKeeperOrGovernor { + _setRate(exchange); + } + + function _setRate(Exchange memory exchange) internal { + rates[exchange.from][exchange.to] = exchange.rate; + rates[exchange.to][exchange.from] = RATE_SCALE / exchange.rate; + + emit RateSet(exchange.from, exchange.to, exchange.rate); + } +} diff --git a/docs/PeriodicAllocationAbstractVault.svg b/docs/PeriodicAllocationAbstractVault.svg index 0a63f40..0710afd 100644 --- a/docs/PeriodicAllocationAbstractVault.svg +++ b/docs/PeriodicAllocationAbstractVault.svg @@ -4,162 +4,165 @@ - - + + UmlClassDiagram - - + + -54 - -<<Abstract>> -AssetPerShareAbstractVault -../contracts/vault/allocate/AssetPerShareAbstractVault.sol - -Public: -   ASSETS_PER_SHARE_SCALE: uint256 -   assetsPerShare: uint256 - -Internal: -    _initialize() -    _previewDeposit(assets: uint256): (shares: uint256) -    _previewMint(shares: uint256): (assets: uint256) -    _previewWithdraw(assets: uint256): (shares: uint256) -    _previewRedeem(shares: uint256): (assets: uint256) -    _convertToAssets(shares: uint256): (assets: uint256) -    _convertToShares(assets: uint256): (shares: uint256) -    _updateAssetPerShare() -External: -    updateAssetPerShare() <<onlyVaultManager>> -Public: -    <<event>> AssetsPerShareUpdated(assetsPerShare: uint256, totalAssets: uint256) +97 + +<<Abstract>> +AssetPerShareAbstractVault +contractsvaultallocateAssetPerShareAbstractVault.sol + +Public: +   ASSETS_PER_SHARE_SCALE: uint256 +   assetsPerShare: uint256 + +Internal: +    _initialize() +    _previewDeposit(assets: uint256): (shares: uint256) +    _previewMint(shares: uint256): (assets: uint256) +    _previewWithdraw(assets: uint256): (shares: uint256) +    _previewRedeem(shares: uint256): (assets: uint256) +    _convertToAssets(shares: uint256): (assets: uint256) +    _convertToShares(assets: uint256): (shares: uint256) +    _updateAssetPerShare() +External: +    updateAssetPerShare() <<onlyVaultManager>> +Public: +    <<event>> AssetsPerShareUpdated(assetsPerShare: uint256, totalAssets: uint256) +    calculateAssetPerShare(): (assetsPerShare_: uint256, totalAssets_: uint256) - + -56 - -<<Struct>> -Settlement -../contracts/vault/allocate/PeriodicAllocationAbstractVault.sol - -vaultIndex: uint256 -assets: uint256 +99 + +<<Struct>> +Settlement +contractsvaultallocatePeriodicAllocationAbstractVault.sol + +vaultIndex: uint256 +assets: uint256 - + -55 - -<<Abstract>> -PeriodicAllocationAbstractVault -../contracts/vault/allocate/PeriodicAllocationAbstractVault.sol - -Public: -   BASIS_SCALE: uint256 -   sourceParams: AssetSourcingParams -   assetsTransferred: uint256 -   assetPerShareUpdateThreshold: uint256 - -Internal: -    _initialize(_underlyingVaults: address[], _sourceParams: AssetSourcingParams, _assetPerShareUpdateThreshold: uint256) -    _deposit(assets: uint256, receiver: address): (shares: uint256) -    _previewDeposit(assets: uint256): (shares: uint256) -    _mint(shares: uint256, receiver: address): (assets: uint256) -    _previewMint(shares: uint256): (assets: uint256) -    _withdraw(assets: uint256, receiver: address, owner: address): (shares: uint256) -    _previewWithdraw(assets: uint256): (shares: uint256) -    _redeem(shares: uint256, receiver: address, owner: address): (assets: uint256) -    _previewRedeem(shares: uint256): (assets: uint256) -    _sourceAssets(assets: uint256, shares: uint256): (actualAssets: uint256) -    _checkAndUpdateAssetPerShare(_assets: uint256) -    _convertToAssets(shares: uint256): (assets: uint256) -    _convertToShares(assets: uint256): (shares: uint256) -    _afterRebalance() -External: -    settle(settlements: Settlement[]) <<onlyVaultManager>> -    setSingleVaultSharesThreshold(_singleVaultSharesThreshold: uint32) <<onlyGovernor>> -    setSingleSourceVaultIndex(_singleSourceVaultIndex: uint32) <<onlyGovernor>> -    setAssetPerShareUpdateThreshold(_assetPerShareUpdateThreshold: uint256) <<onlyGovernor>> -Public: -    <<event>> SingleVaultSharesThresholdUpdated(singleVaultSharesThreshold: uint256) -    <<event>> SingleSourceVaultIndexUpdated(singleSourceVaultIndex: uint32) -    <<event>> AssetPerShareUpdateThresholdUpdated(assetPerShareUpdateThreshold: uint256) +98 + +<<Abstract>> +PeriodicAllocationAbstractVault +contractsvaultallocatePeriodicAllocationAbstractVault.sol + +Public: +   BASIS_SCALE: uint256 +   sourceParams: AssetSourcingParams +   assetsTransferred: uint256 +   assetPerShareUpdateThreshold: uint256 + +Internal: +    _initialize(_underlyingVaults: address[], _sourceParams: AssetSourcingParams, _assetPerShareUpdateThreshold: uint256) +    _deposit(assets: uint256, receiver: address): (shares: uint256) +    _previewDeposit(assets: uint256): (shares: uint256) +    _mint(shares: uint256, receiver: address): (assets: uint256) +    _previewMint(shares: uint256): (assets: uint256) +    _withdraw(assets: uint256, receiver: address, owner: address): (shares: uint256) +    _previewWithdraw(assets: uint256): (shares: uint256) +    _redeem(shares: uint256, receiver: address, owner: address): (assets: uint256) +    _previewRedeem(shares: uint256): (assets: uint256) +    _sourceAssets(assets: uint256, shares: uint256): (actualAssets: uint256) +    _checkAndUpdateAssetPerShare(_assets: uint256) +    _convertToAssets(shares: uint256): (assets: uint256) +    _convertToShares(assets: uint256): (shares: uint256) +    _afterRebalance() +    _afterRemoveVault() +External: +    settle(settlements: Settlement[]) <<onlyVaultManager>> +    setSingleVaultSharesThreshold(_singleVaultSharesThreshold: uint32) <<onlyGovernor>> +    setSingleSourceVaultIndex(_singleSourceVaultIndex: uint32) <<onlyGovernor>> +    setAssetPerShareUpdateThreshold(_assetPerShareUpdateThreshold: uint256) <<onlyGovernor>> +Public: +    <<event>> SingleVaultSharesThresholdUpdated(singleVaultSharesThreshold: uint256) +    <<event>> SingleSourceVaultIndexUpdated(singleSourceVaultIndex: uint32) +    <<event>> AssetPerShareUpdateThresholdUpdated(assetPerShareUpdateThreshold: uint256) - + -56->55 - - +99->98 + + - + -57 - -<<Struct>> -AssetSourcingParams -../contracts/vault/allocate/PeriodicAllocationAbstractVault.sol - -singleVaultSharesThreshold: uint32 -singleSourceVaultIndex: uint32 +100 + +<<Struct>> +AssetSourcingParams +contractsvaultallocatePeriodicAllocationAbstractVault.sol + +singleVaultSharesThreshold: uint32 +singleSourceVaultIndex: uint32 - + -57->55 - - +100->98 + + - + -55->54 - - +98->97 + + - + -55->56 - - +98->99 + + - + -55->57 - - +98->100 + + - + -59 - -<<Abstract>> -SameAssetUnderlyingsAbstractVault -../contracts/vault/allocate/SameAssetUnderlyingsAbstractVault.sol - -Internal: -   _activeUnderlyingVaults: IERC4626Vault[] -   vaultIndexMap: uint256 - -Internal: -    _initialize(_underlyingVaults: address[]) -    _totalUnderlyingAssets(): (totalUnderlyingAssets: uint256) -    _addVault(_underlyingVault: address, _vaultIndexMap: uint256): (vaultIndexMap_: uint256) -    _afterRebalance() -External: -    activeUnderlyingVaults(): (activeVaults: uint256) -    totalUnderlyingVaults(): (totalVaults: uint256) -    rebalance(swaps: Swap[]) <<onlyVaultManager>> -    addVault(_underlyingVault: address) <<onlyGovernor>> -    removeVault(vaultIndex: uint256) <<onlyGovernor>> -Public: -    <<event>> AddedVault(vaultIndex: uint256, vault: address) -    <<event>> RemovedVault(vaultIndex: uint256, vault: address) -    totalAssets(): (totalManagedAssets: uint256) -    resolveVaultIndex(vaultIndex: uint256): (vault: IERC4626Vault) +102 + +<<Abstract>> +SameAssetUnderlyingsAbstractVault +contractsvaultallocateSameAssetUnderlyingsAbstractVault.sol + +Internal: +   _activeUnderlyingVaults: IERC4626Vault[] +   vaultIndexMap: uint256 + +Internal: +    _initialize(_underlyingVaults: address[]) +    _totalUnderlyingAssets(): (totalUnderlyingAssets: uint256) +    _addVault(_underlyingVault: address, _vaultIndexMap: uint256): (vaultIndexMap_: uint256) +    _afterRebalance() +    _afterRemoveVault() +External: +    activeUnderlyingVaults(): (activeVaults: uint256) +    totalUnderlyingVaults(): (totalVaults: uint256) +    rebalance(swaps: Swap[]) <<onlyVaultManager>> +    addVault(_underlyingVault: address) <<onlyGovernor>> +    removeVault(vaultIndex: uint256) <<onlyGovernor>> +Public: +    <<event>> AddedVault(vaultIndex: uint256, vault: address) +    <<event>> RemovedVault(vaultIndex: uint256, vault: address) +    totalAssets(): (totalManagedAssets: uint256) +    resolveVaultIndex(vaultIndex: uint256): (vault: IERC4626Vault) - + -55->59 - - +98->102 + + diff --git a/docs/metaVaultChargePerformanceFee.png b/docs/metaVaultChargePerformanceFee.png new file mode 100644 index 0000000..9113b24 Binary files /dev/null and b/docs/metaVaultChargePerformanceFee.png differ diff --git a/docs/metaVaultUpdateAssetsPerShare.png b/docs/metaVaultUpdateAssetsPerShare.png index e95753b..db913ad 100644 Binary files a/docs/metaVaultUpdateAssetsPerShare.png and b/docs/metaVaultUpdateAssetsPerShare.png differ diff --git a/docs/templates/common.hbs b/docs/templates/common.hbs new file mode 100644 index 0000000..38ad97f --- /dev/null +++ b/docs/templates/common.hbs @@ -0,0 +1,34 @@ +{{h}} {{name}} + +{{#if signature}} +```solidity +{{{signature}}} +``` +{{/if}} + +{{{natspec.notice}}} + +{{!-- {{#if natspec.dev}} +_{{{natspec.dev}}}_ +{{/if}} --}} + +{{#if natspec.params}} +{{h 2}} Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +{{#each params}} +| {{name}} | {{type}} | {{{joinLines natspec}}} | +{{/each}} +{{/if}} + +{{#if natspec.returns}} +{{h 2}} Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +{{#each returns}} +| {{#if name}}{{name}}{{else}}[{{@index}}]{{/if}} | {{type}} | {{{joinLines natspec}}} | +{{/each}} + +{{/if}} \ No newline at end of file diff --git a/docs/templates/contract.hbs b/docs/templates/contract.hbs new file mode 100644 index 0000000..0e08ead --- /dev/null +++ b/docs/templates/contract.hbs @@ -0,0 +1,9 @@ +{{>common}} + +{{#each items}} +{{#unless (eq visibility "internal")}} +{{#hsection}} +{{>item}} +{{/hsection}} +{{/unless}} +{{/each}} \ No newline at end of file diff --git a/docs/templates/helpers.js b/docs/templates/helpers.js new file mode 100644 index 0000000..3efa428 --- /dev/null +++ b/docs/templates/helpers.js @@ -0,0 +1,14 @@ +const { findAll } = require('solidity-ast/utils'); + +module.exports = { + eq: (a, b) => a === b, + + /** @this {import('solidity-docgen').DocItemWithContext} */ + allEvents() { + if (this.nodeType === 'ContractDefinition') { + const { deref } = this.__item_context.build; + const parents = this.linearizedBaseContracts.map(deref('ContractDefinition')); + return parents.flatMap(p => [...findAll('EventDefinition', p)]); + } + }, +}; \ No newline at end of file diff --git a/hardhat.config.ts b/hardhat.config.ts index 20041ed..8281b3f 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -8,6 +8,7 @@ import "hardhat-abi-exporter" import "@nomiclabs/hardhat-etherscan" import "ts-node/register" import "tsconfig-paths/register" +import "solidity-docgen" import { config as dotenvConfig } from "dotenv" import { resolve } from "path" @@ -81,6 +82,10 @@ export const hardhatConfig = { etherscan: { apiKey: process.env.ETHERSCAN_KEY, }, + docgen: { + outputDir: "./docs/natspec", + templates: "./docs/templates", + }, } export default hardhatConfig diff --git a/package.json b/package.json index d47fb12..43c4226 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@mstable/metavaults", - "version": "0.0.5", + "version": "0.0.6", "description": "mStable EIP-4626 Tokenized Vaults", "author": "mStable ", "license": "AGPL-3.0-or-later", @@ -46,7 +46,8 @@ "test:file": "yarn hardhat test --typecheck", "slither": "slither .", "prepublishOnly": "yarn compile && yarn compile-ts && yarn compile-abis && yarn copy-types && npx replace-tsconfig-paths", - "web": "ts-node ./web-config.ts" + "web": "ts-node ./web-config.ts", + "docgen": "yarn hardhat docgen" }, "repository": { "type": "git", @@ -91,6 +92,7 @@ "sol-merger": "^4.1.1", "solc": "0.8.17", "solhint": "^3.3.7", + "solidity-docgen": "^0.6.0-beta.29", "ts-generator": "^0.1.1", "typescript": "^4.8.4" }, diff --git a/tasks/convex3CrvMetaVault.ts b/tasks/convex3CrvMetaVault.ts index 0e1f1ed..d297bcb 100644 --- a/tasks/convex3CrvMetaVault.ts +++ b/tasks/convex3CrvMetaVault.ts @@ -1,7 +1,7 @@ import { simpleToExactAmount } from "@utils/math" import { formatUnits } from "ethers/lib/utils" import { subtask, task, types } from "hardhat/config" -import { AssetProxy__factory, IERC20__factory, PeriodicAllocationPerfFeeMetaVault__factory } from "types/generated" +import { AssetProxy__factory, IERC20__factory, IERC4626Vault__factory, PeriodicAllocationPerfFeeMetaVault__factory } from "types/generated" import { config } from "./deployment/convex3CrvVaults-config" import { usdFormatter } from "./utils" @@ -36,6 +36,7 @@ interface PeriodicAllocationPerfFeeMetaVaultParams { underlyingVaults: Array sourceParams: AssetSourcingParams assetPerShareUpdateThreshold: BN + proxy: boolean } export async function deployPeriodicAllocationPerfFeeMetaVaults( @@ -60,6 +61,7 @@ export async function deployPeriodicAllocationPerfFeeMetaVaults( sourceParams: PeriodicAllocationPerfFeeMetaVaultConf.sourceParams, assetPerShareUpdateThreshold: PeriodicAllocationPerfFeeMetaVaultConf.assetPerShareUpdateThreshold, underlyingVaults, + proxy: true, }) return periodicAllocationPerfFeeMetaVault } @@ -81,6 +83,7 @@ export const deployPeriodicAllocationPerfFeeMetaVault = async ( underlyingVaults, sourceParams, assetPerShareUpdateThreshold, + proxy, } = params const constructorArguments = [nexus, asset] const vaultImpl = await deployContract( @@ -96,6 +99,9 @@ export const deployPeriodicAllocationPerfFeeMetaVault = async ( }) // Proxy + if (!proxy) { + return { proxy: undefined, impl: vaultImpl } + } const data = vaultImpl.interface.encodeFunctionData("initialize", [ name, symbol, @@ -107,9 +113,9 @@ export const deployPeriodicAllocationPerfFeeMetaVault = async ( assetPerShareUpdateThreshold, ]) const proxyConstructorArguments = [vaultImpl.address, proxyAdmin, data] - const proxy = await deployContract(new AssetProxy__factory(signer), "AssetProxy", proxyConstructorArguments) + const proxyContract = await deployContract(new AssetProxy__factory(signer), "AssetProxy", proxyConstructorArguments) - return { proxy, impl: vaultImpl } + return { proxy: proxyContract, impl: vaultImpl } } subtask("convex-3crv-mv-deploy", "Deploys Convex 3Crv Meta Vault") @@ -134,6 +140,7 @@ subtask("convex-3crv-mv-deploy", "Deploys Convex 3Crv Meta Vault") ) .addOptionalParam("updateThreshold", "Asset per share update threshold. default 100k", 100000, types.int) .addOptionalParam("vaultManager", "Name or address to override the Vault Manager", "VaultManager", types.string) + .addOptionalParam("proxy", "Deploy a proxy contract", true, types.boolean) .addOptionalParam("speed", "Defender Relayer speed param: 'safeLow' | 'average' | 'fast' | 'fastest'", "fast", types.string) .setAction(async (taskArgs, hre) => { const { @@ -148,6 +155,7 @@ subtask("convex-3crv-mv-deploy", "Deploys Convex 3Crv Meta Vault") singleThreshold, updateThreshold, vaultManager, + proxy, speed, } = taskArgs @@ -166,7 +174,7 @@ subtask("convex-3crv-mv-deploy", "Deploys Convex 3Crv Meta Vault") const feeReceiverAddress = resolveAddress(feeReceiver, chain) - const { proxy, impl } = await deployPeriodicAllocationPerfFeeMetaVault(hre, signer, { + const { proxy: proxyContract, impl } = await deployPeriodicAllocationPerfFeeMetaVault(hre, signer, { nexus: nexusAddress, asset: assetToken.address, name, @@ -181,9 +189,10 @@ subtask("convex-3crv-mv-deploy", "Deploys Convex 3Crv Meta Vault") singleSourceVaultIndex, }, assetPerShareUpdateThreshold: simpleToExactAmount(updateThreshold, assetToken.decimals), + proxy, }) - return { proxy, impl } + return { proxy: proxyContract, impl } }) task("convex-3crv-mv-deploy").setAction(async (_, __, runSuper) => { return runSuper() @@ -203,6 +212,9 @@ subtask("convex-3crv-mv-snap", "Logs Convex 3Crv Meta Vault details") const vaultToken = await resolveAssetToken(signer, chain, vault) const vaultContract = PeriodicAllocationPerfFeeMetaVault__factory.connect(vaultToken.address, signer) + const fraxVaultContract = IERC4626Vault__factory.connect(resolveAddress("vcx3CRV-FRAX"), signer) + const musdVaultContract = IERC4626Vault__factory.connect(resolveAddress("vcx3CRV-mUSD"), signer) + const busdVaultContract = IERC4626Vault__factory.connect(resolveAddress("vcx3CRV-BUSD"), signer) const assetToken = await resolveAssetToken(signer, chain, vaultToken.assetSymbol) const assetContract = IERC20__factory.connect(assetToken.address, signer) @@ -212,6 +224,7 @@ subtask("convex-3crv-mv-snap", "Logs Convex 3Crv Meta Vault details") }) console.log(`\nPeriodicAllocationPerfFeeMetaVault`) + // Assets const assetsInVault = await assetContract.balanceOf(vaultToken.address, { blockTag: blk.blockNumber, }) @@ -231,6 +244,35 @@ subtask("convex-3crv-mv-snap", "Logs Convex 3Crv Meta Vault details") 2, )}%`, ) + const fraxAssets = await fraxVaultContract.maxWithdraw(vaultContract.address, { + blockTag: blk.blockNumber, + }) + console.log( + `Assets in FRAX vault : ${usdFormatter(fraxAssets, assetToken.decimals)} ${formatUnits( + fraxAssets.mul(10000).div(totalAssets), + 2, + )}%`, + ) + const busdAssets = await busdVaultContract.maxWithdraw(vaultContract.address, { + blockTag: blk.blockNumber, + }) + console.log( + `Assets in BUSD vault : ${usdFormatter(busdAssets, assetToken.decimals)} ${formatUnits( + busdAssets.mul(10000).div(totalAssets), + 2, + )}%`, + ) + const musdAssets = await musdVaultContract.maxWithdraw(vaultContract.address, { + blockTag: blk.blockNumber, + }) + console.log( + `Assets in mUSD vault : ${usdFormatter(musdAssets, assetToken.decimals)} ${formatUnits( + musdAssets.mul(10000).div(totalAssets), + 2, + )}%`, + ) + + // Assets per share console.log( `stored assets/share : ${formatUnits( await vaultContract.assetsPerShare({ @@ -243,15 +285,15 @@ subtask("convex-3crv-mv-snap", "Logs Convex 3Crv Meta Vault details") blockTag: blk.blockNumber, }) console.log(`current assets/share : ${formatUnits(current.assetsPerShare_, 26)}`) + const perfAssetsPerShare = await vaultContract.perfFeesAssetPerShare({ + blockTag: blk.blockNumber, + }) + const perfPercentage = current.assetsPerShare_.sub(perfAssetsPerShare).mul(1000000).div(perfAssetsPerShare) + console.log(`performance assets/share: ${formatUnits(perfAssetsPerShare, 26)} ${formatUnits(perfPercentage, 4)}%`) + const fee = await vaultContract.performanceFee({ blockTag: blk.blockNumber, }) - console.log(`Performance fee : ${fee.toNumber() / 10000}%`) - console.log( - `Fee receiver : ${await vaultContract.feeReceiver({ - blockTag: blk.blockNumber, - })}`, - ) console.log( `Active underlying vaults: ${await vaultContract.activeUnderlyingVaults({ blockTag: blk.blockNumber, @@ -274,6 +316,23 @@ subtask("convex-3crv-mv-snap", "Logs Convex 3Crv Meta Vault details") blockTag: blk.blockNumber, })}`, ) + + console.log(`\nPerformance fee : ${fee.toNumber() / 10000}%`) + const feeReceiver = await vaultContract.feeReceiver({ + blockTag: blk.blockNumber, + }) + console.log(`Fee receiver : ${feeReceiver}`) + const feeShares = await vaultContract.balanceOf(feeReceiver, { + blockTag: blk.blockNumber, + }) + const feeAssets = await vaultContract.maxWithdraw(feeReceiver, { + blockTag: blk.blockNumber, + }) + console.log( + `Collected fees : ${formatUnits(feeShares)} shares, ${formatUnits(feeAssets, assetToken.decimals)} ${ + assetToken.symbol + }`, + ) }) task("convex-3crv-mv-snap").setAction(async (_, __, runSuper) => { return runSuper() diff --git a/tasks/convex3CrvVault.ts b/tasks/convex3CrvVault.ts index 7300ea1..4ece1e3 100644 --- a/tasks/convex3CrvVault.ts +++ b/tasks/convex3CrvVault.ts @@ -1,4 +1,6 @@ import { ONE_DAY } from "@utils/constants" +import { BN, simpleToExactAmount } from "@utils/math" +import { formatUnits } from "ethers/lib/utils" import { subtask, task, types } from "hardhat/config" import { AssetProxy__factory, @@ -10,9 +12,10 @@ import { import { config } from "./deployment/convex3CrvVaults-config" import { CRV, CVX } from "./utils" +import { getBlock } from "./utils/blocks" import { deployContract } from "./utils/deploy-utils" import { verifyEtherscan } from "./utils/etherscan" -import { getChain, resolveAddress } from "./utils/networkAddressFactory" +import { getChain, resolveAddress, resolveAssetToken } from "./utils/networkAddressFactory" import { getSigner } from "./utils/signerFactory" import type { Signer } from "ethers" @@ -50,6 +53,7 @@ interface Convex3CrvBasicVaultParams { interface Convex3CrvLiquidatorVaultParams extends Convex3CrvBasicVaultParams { streamDuration: number + proxy: boolean } export async function deployCurve3CrvMetapoolCalculatorLibrary(hre: HardhatRuntimeEnvironment, signer: Signer) { @@ -138,6 +142,7 @@ export async function deployConvex3CrvLiquidatorVault( donateToken, donationFee, feeReceiver, + proxy, } = params const linkAddresses = getMetapoolLinkAddresses(calculatorLibrary) @@ -157,6 +162,9 @@ export async function deployConvex3CrvLiquidatorVault( }) // Proxy + if (!proxy) { + return { proxy: undefined, impl: vaultImpl } + } const data = vaultImpl.interface.encodeFunctionData("initialize", [ name, symbol, @@ -168,9 +176,9 @@ export async function deployConvex3CrvLiquidatorVault( donationFee, ]) const proxyConstructorArguments = [vaultImpl.address, proxyAdmin, data] - const proxy = await deployContract(new AssetProxy__factory(signer), "AssetProxy", proxyConstructorArguments) + const proxyContract = await deployContract(new AssetProxy__factory(signer), "AssetProxy", proxyConstructorArguments) - return { proxy, impl: vaultImpl } + return { proxy: proxyContract, impl: vaultImpl } } subtask("convex-3crv-lib-deploy", "Deploys a Curve Metapool calculator library") @@ -207,6 +215,7 @@ subtask("convex-3crv-vault-deploy", "Deploys Convex 3Crv Liquidator Vault") .addOptionalParam("fee", "Liquidation fee scaled to 6 decimal places. default 16% = 160000", 160000, types.int) .addOptionalParam("feeReceiver", "Address or name of account that will receive vault fees.", "mStableDAO", types.string) .addOptionalParam("vaultManager", "Name or address to override the Vault Manager", "VaultManager", types.string) + .addOptionalParam("proxy", "Deploy a proxy contract", true, types.boolean) .addOptionalParam("speed", "Defender Relayer speed param: 'safeLow' | 'average' | 'fast' | 'fastest'", "fast", types.string) .setAction(async (taskArgs, hre) => { const { @@ -222,6 +231,7 @@ subtask("convex-3crv-vault-deploy", "Deploys Convex 3Crv Liquidator Vault") fee, feeReceiver, vaultManager, + proxy, speed, } = taskArgs @@ -253,7 +263,7 @@ subtask("convex-3crv-vault-deploy", "Deploys Convex 3Crv Liquidator Vault") const rewardTokens = [CRV.address, CVX.address] // Vault library - const { proxy, impl } = await deployConvex3CrvLiquidatorVault(hre, signer, { + const { proxy: proxyContract, impl } = await deployConvex3CrvLiquidatorVault(hre, signer, { calculatorLibrary: calculatorLibraryAddress, nexus: nexusAddress, asset: assetAddress, @@ -269,10 +279,111 @@ subtask("convex-3crv-vault-deploy", "Deploys Convex 3Crv Liquidator Vault") rewardTokens, donationFee: fee, feeReceiver: feeReceiverAddress, + proxy, }) - return { proxy, impl } + return { proxyContract, impl } }) task("convex-3crv-vault-deploy").setAction(async (_, __, runSuper) => { return runSuper() }) + +const secondsToBurn = (blocktime: number, stream: { last: number; end: number; sharesPerSecond: BN }) => { + if (blocktime < stream.end) { + return blocktime - stream.last + } else if (stream.last < stream.end) { + return stream.end - stream.last + } + return 0 +} + +subtask("convex-3crv-snap", "Logs Convex 3Crv Vault details") + .addParam("vault", "Vault symbol or address", undefined, types.string) + .addOptionalParam("owner", "Address, contract name or token symbol to get balances for. Defaults to signer", undefined, types.string) + .addOptionalParam("block", "Block number. (default: current block)", 0, types.int) + .setAction(async (taskArgs, hre) => { + const { vault, owner, block, speed } = taskArgs + + const signer = await getSigner(hre, speed) + const chain = getChain(hre) + + const blk = await getBlock(hre.ethers, block) + + const vaultToken = await resolveAssetToken(signer, chain, vault) + const vaultContract = Convex3CrvLiquidatorVault__factory.connect(vaultToken.address, signer) + const assetToken = await resolveAssetToken(signer, chain, vaultToken.assetSymbol) + + await hre.run("vault-snap", { + vault, + owner, + }) + + console.log(`\nConvex3CrvLiquidatorVault`) + // Assets per share + const totalShares = await vaultContract.totalSupply({ + blockTag: blk.blockNumber, + }) + const totalAssets = await vaultContract.totalAssets({ + blockTag: blk.blockNumber, + }) + const assetsPerShare = totalAssets.mul(simpleToExactAmount(1)).div(totalShares) + console.log(`Assets per share : ${formatUnits(assetsPerShare).padStart(21)}`) + + // Stream data + const stream = await vaultContract.shareStream({ + blockTag: blk.blockNumber, + }) + const streamDuration = await vaultContract.STREAM_DURATION() + const streamScale = await vaultContract.STREAM_PER_SECOND_SCALE() + const streamTotal = stream.sharesPerSecond.mul(streamDuration).div(streamScale) + const sharesStillStreaming = await vaultContract.streamedShares({ + blockTag: blk.blockNumber, + }) + const streamRemainingPercentage = streamTotal.gt(0) ? sharesStillStreaming.mul(10000).div(streamTotal) : BN.from(0) + const streamBurnable = stream.sharesPerSecond.mul(secondsToBurn(blk.blockTimestamp, stream)).div(streamScale) + const streamBurnablePercentage = streamTotal.gt(0) ? streamBurnable.mul(10000).div(streamTotal) : BN.from(0) + + console.log(`Stream total : ${formatUnits(streamTotal).padStart(21)} shares`) + console.log(`Stream burnable : ${formatUnits(streamBurnable).padStart(21)} shares ${formatUnits(streamBurnablePercentage, 2)}%`) + console.log( + `Stream remaining : ${formatUnits(sharesStillStreaming).padStart(21)} shares ${formatUnits(streamRemainingPercentage, 2)}%`, + ) + console.log(`Stream last : ${new Date(stream.last * 1000)}`) + console.log(`Stream end : ${new Date(stream.end * 1000)}`) + + // Rewards + console.log("\nRewards accrued:") + const rewards = await vaultContract.callStatic.collectRewards({ + blockTag: blk.blockNumber, + }) + let i = 0 + for (const reward of rewards.rewardTokens_) { + const rewardToken = await resolveAssetToken(signer, chain, reward) + console.log(` ${formatUnits(rewards.rewards[i], rewardToken.decimals)} ${rewardToken.symbol}`) + i++ + } + const donateToken = await resolveAssetToken(signer, chain, rewards.donateTokens[0]) + console.log(` Rewards are swapped for : ${donateToken.symbol}`) + + // Fees + const fee = await vaultContract.donationFee({ + blockTag: blk.blockNumber, + }) + console.log(`\nLiquidation fee : ${fee / 10000}%`) + const feeReceiver = await vaultContract.feeReceiver({ + blockTag: blk.blockNumber, + }) + const feeShares = await vaultContract.balanceOf(feeReceiver, { + blockTag: blk.blockNumber, + }) + const feeAssets = await vaultContract.maxWithdraw(feeReceiver, { + blockTag: blk.blockNumber, + }) + console.log( + `Collected fees : ${formatUnits(feeShares)} shares, ${formatUnits(feeAssets, assetToken.decimals)} ${assetToken.symbol}`, + ) + }) + +task("convex-3crv-snap").setAction(async (_, __, runSuper) => { + return runSuper() +}) diff --git a/tasks/curve3CrvVault.ts b/tasks/curve3CrvVault.ts index 3d38f25..da7cb0e 100644 --- a/tasks/curve3CrvVault.ts +++ b/tasks/curve3CrvVault.ts @@ -28,6 +28,7 @@ interface Curve3CrvBasicMetaVaultParams { symbol: string vaultManager: string proxyAdmin: string + proxy: boolean } interface Curve3CrvMetaVaultDeployed { proxy: AssetProxy @@ -54,7 +55,7 @@ export async function deployCurve3PoolCalculatorLibrary(hre: HardhatRuntimeEnvir } export const deployCurve3CrvMetaVault = async (hre: HardhatRuntimeEnvironment, signer: Signer, params: Curve3CrvBasicMetaVaultParams) => { - const { calculatorLibrary, nexus, asset, metaVault, slippageData, name, symbol, vaultManager, proxyAdmin } = params + const { calculatorLibrary, nexus, asset, metaVault, slippageData, name, symbol, vaultManager, proxyAdmin, proxy } = params const libraryAddresses = { "contracts/peripheral/Curve/Curve3PoolCalculatorLibrary.sol:Curve3PoolCalculatorLibrary": calculatorLibrary } @@ -72,11 +73,14 @@ export const deployCurve3CrvMetaVault = async (hre: HardhatRuntimeEnvironment, s }) // Proxy + if (!proxy) { + return { proxy: undefined, impl: vaultImpl } + } const data = vaultImpl.interface.encodeFunctionData("initialize", [name, symbol, vaultManager, slippageData]) const proxyConstructorArguments = [vaultImpl.address, proxyAdmin, data] - const proxy = await deployContract(new AssetProxy__factory(signer), "AssetProxy", proxyConstructorArguments) + const proxyContract = await deployContract(new AssetProxy__factory(signer), "AssetProxy", proxyConstructorArguments) - return { proxy, impl: vaultImpl } + return { proxy: proxyContract, impl: vaultImpl } } export async function deployCurve3CrvMetaVaults( @@ -107,6 +111,7 @@ export async function deployCurve3CrvMetaVaults( symbol: curve3CrvPool.symbol, vaultManager, proxyAdmin, + proxy: true, }) curve3CrvMetaVaults[pool] = curve3CrvMetaVaultDeployed } @@ -135,9 +140,10 @@ subtask("curve-3crv-meta-vault-deploy", "Deploys Curve 3Pool Meta Vault") .addOptionalParam("calculatorLibrary", "Name or address of the Curve calculator library.", "Curve3CrvCalculatorLibrary", types.string) .addOptionalParam("slippage", "Max slippage in basis points. default 1% = 100", 100, types.int) .addOptionalParam("vaultManager", "Name or address to override the Vault Manager", "VaultManager", types.string) + .addOptionalParam("proxy", "Deploy a proxy contract", true, types.boolean) .addOptionalParam("speed", "Defender Relayer speed param: 'safeLow' | 'average' | 'fast' | 'fastest'", "fast", types.string) .setAction(async (taskArgs, hre) => { - const { metaVault, name, symbol, asset, calculatorLibrary, slippage, admin, vaultManager, speed } = taskArgs + const { metaVault, name, symbol, asset, calculatorLibrary, slippage, admin, vaultManager, proxy, speed } = taskArgs const signer = await getSigner(hre, speed) const chain = getChain(hre) @@ -149,7 +155,7 @@ subtask("curve-3crv-meta-vault-deploy", "Deploys Curve 3Pool Meta Vault") const metaVaultAddress = resolveAddress(metaVault, chain) const calculatorLibraryAddress = resolveAddress(calculatorLibrary, chain) - const { proxy, impl } = await deployCurve3CrvMetaVault(hre, signer, { + const { proxy: proxyContract, impl } = await deployCurve3CrvMetaVault(hre, signer, { nexus: nexusAddress, asset: assetToken.address, name, @@ -159,9 +165,10 @@ subtask("curve-3crv-meta-vault-deploy", "Deploys Curve 3Pool Meta Vault") proxyAdmin: proxyAdminAddress, slippageData: { mint: slippage, deposit: slippage, redeem: slippage, withdraw: slippage }, calculatorLibrary: calculatorLibraryAddress, + proxy, }) - return { proxy, impl } + return { proxyContract, impl } }) task("curve-3crv-meta-vault-deploy").setAction(async (_, __, runSuper) => { return runSuper() diff --git a/tasks/deployment/convex3CrvVaults.ts b/tasks/deployment/convex3CrvVaults.ts index b7dff6a..a9e77e5 100644 --- a/tasks/deployment/convex3CrvVaults.ts +++ b/tasks/deployment/convex3CrvVaults.ts @@ -121,6 +121,7 @@ const deployerConvex3CrvVault = feeReceiver: resolveAddress("mStableDAO"), donationFee: 10000, factory: convex3CrvPool.isFactory, + proxy: true, }) } export async function deployConvex3CrvVaults( @@ -182,7 +183,15 @@ export const deployCommon = async ( const cowSwapDex = !!asyncSwapperAddress ? new CowSwapDex__factory(signer).attach(asyncSwapperAddress) : await deployCowSwapDex(hre, signer, nexus.address) - const liquidator = await deployLiquidator(hre, signer, nexus.address, oneInchDexSwap.address, cowSwapDex.address, proxyAdmin.address) + const liquidator = await deployLiquidator( + hre, + signer, + nexus.address, + oneInchDexSwap.address, + cowSwapDex.address, + proxyAdmin.address, + true, + ) return { oneInchDexSwap, cowSwapDex, liquidator } } diff --git a/tasks/liquidator.ts b/tasks/liquidator.ts index a627373..9456733 100644 --- a/tasks/liquidator.ts +++ b/tasks/liquidator.ts @@ -9,7 +9,6 @@ import { AssetProxy__factory, IERC20Metadata__factory, Liquidator__factory } fro import { getOrder, placeSellOrder } from "./peripheral/cowswapApi" import { OneInchRouter } from "./peripheral/oneInchApi" import { verifyEtherscan } from "./utils/etherscan" -import { buildDonateTokensInput } from "./utils/liquidatorUtil" import { logger } from "./utils/logger" import { getChain, resolveAddress, resolveAssetToken } from "./utils/networkAddressFactory" import { getSigner } from "./utils/signerFactory" @@ -33,6 +32,7 @@ export async function deployLiquidator( syncSwapperAddress: string, asyncSwapperAddress: string, proxyAdmin: string, + proxy = true, ) { const constructorArguments = [nexusAddress] const liquidatorImpl = await deployContract(new Liquidator__factory(signer), "Liquidator", constructorArguments) @@ -44,17 +44,20 @@ export async function deployLiquidator( }) // Proxy + if (!proxy) { + return liquidatorImpl + } const data = liquidatorImpl.interface.encodeFunctionData("initialize", [syncSwapperAddress, asyncSwapperAddress]) const proxyConstructorArguments = [liquidatorImpl.address, proxyAdmin, data] - const proxy = await deployContract(new AssetProxy__factory(signer), "AssetProxy", proxyConstructorArguments) + const proxyContract = await deployContract(new AssetProxy__factory(signer), "AssetProxy", proxyConstructorArguments) await verifyEtherscan(hre, { - address: proxy.address, + address: proxyContract.address, contract: "contracts/upgradability/Proxies.sol:AssetProxy", constructorArguments: proxyConstructorArguments, }) - return Liquidator__factory.connect(proxy.address, signer) + return Liquidator__factory.connect(proxyContract.address, signer) } subtask("liq-deploy", "Deploys a new Liquidator contract") @@ -62,9 +65,10 @@ subtask("liq-deploy", "Deploys a new Liquidator contract") .addOptionalParam("asyncSwapper", "Async Swapper address override", "CowSwapDex", types.string) .addOptionalParam("nexus", "Nexus address override", "Nexus", types.string) .addOptionalParam("admin", "Proxy admin name or address override. eg DelayedProxyAdmin", "InstantProxyAdmin", types.string) + .addOptionalParam("proxy", "Deploy a proxy contract", true, types.boolean) .addOptionalParam("speed", "Defender Relayer speed param: 'safeLow' | 'average' | 'fast' | 'fastest'", "fast", types.string) .setAction(async (taskArgs, hre) => { - const { nexus, admin, syncSwapper, asyncSwapper, speed } = taskArgs + const { nexus, admin, syncSwapper, asyncSwapper, proxy, speed } = taskArgs const chain = getChain(hre) const signer = await getSigner(hre, speed) const nexusAddress = resolveAddress(nexus, chain) @@ -72,7 +76,7 @@ subtask("liq-deploy", "Deploys a new Liquidator contract") const asyncSwapperAddress = resolveAddress(asyncSwapper, chain) const proxyAdminAddress = resolveAddress(admin, chain) - return deployLiquidator(hre, signer, nexusAddress, syncSwapperAddress, asyncSwapperAddress, proxyAdminAddress) + return deployLiquidator(hre, signer, nexusAddress, syncSwapperAddress, asyncSwapperAddress, proxyAdminAddress, proxy) }) task("liq-deploy").setAction(async (_, __, runSuper) => { @@ -95,10 +99,18 @@ subtask("liq-collect-rewards", "Collect rewards from vaults") const receipt = await tx.wait() const events = receipt.events?.find((e) => e.event === "CollectedRewards") - events?.args?.rewards.forEach((rewards, i) => { - // TODO include reward symbol, formatted amounts and vault symbol - log(`Collected ${rewards} rewards from vault ${i}`) - }) + + let i = 0 + for (const rewards of events?.args?.rewards) { + let j = 0 + for (const reward of rewards) { + const vaultToken = await resolveAssetToken(signer, chain, vaultsAddress[i]) + const rewardToken = await resolveAssetToken(signer, chain, events?.args?.rewardTokens[i][j]) + log(`Collected ${formatUnits(reward, rewardToken.decimals)} ${rewardToken.symbol} rewards from vault ${vaultToken.symbol}`) + j++ + } + i++ + } }) task("liq-collect-rewards").setAction(async (_, __, runSuper) => { await runSuper() @@ -116,9 +128,11 @@ subtask("liq-init-swap", "Initiate CowSwap swap of rewards to donate tokens") .addOptionalParam("transfer", "Transfer sell tokens from liquidator?.", true, types.boolean) .addOptionalParam("liquidator", "Liquidator address override", "LiquidatorV2", types.string) .addOptionalParam("swapper", "Name or address to override the CowSwapDex contract", "CowSwapDex", types.string) + .addOptionalParam("readonly", "Quote swap but not initiate.", false, types.boolean) + .addOptionalParam("maxFee", "Max fee in from tokens", 0, types.int) .addOptionalParam("speed", "Defender Relayer speed param: 'safeLow' | 'average' | 'fast' | 'fastest'", "fast", types.string) .setAction(async (taskArgs, hre) => { - const { from, to, liquidator, receiver, transfer, swapper, speed } = taskArgs + const { from, to, liquidator, maxFee, readonly, receiver, transfer, swapper, speed } = taskArgs const chain = getChain(hre) const signer = await getSigner(hre, speed) const liquidatorAddress = resolveAddress(liquidator, chain) @@ -149,14 +163,25 @@ subtask("liq-init-swap", "Initiate CowSwap swap of rewards to donate tokens") log(`Sell ${formatUnits(rewards, sellToken.decimals)} ${sellToken.symbol} rewards`) const sellOrder = await placeSellOrder(context, sellOrderParams) log(`uid ${sellOrder.orderUid}`) - log(`fee ${formatUnits(sellOrder.fromAssetFeeAmount, sellToken.decimals)}`) + const feePercentage = sellOrder.fromAssetFeeAmount.mul(10000).div(rewards) + log(`fee ${formatUnits(sellOrder.fromAssetFeeAmount, sellToken.decimals)} ${formatUnits(feePercentage, 2)}%`) log(`buy ${formatUnits(sellOrder.toAssetAmountAfterFee, buyToken.decimals)} ${buyToken.symbol}`) - // Initiate the order and sign - const data = encodeInitiateSwap(sellOrder.orderUid, transfer) - const tx = await liquidatorContract.initiateSwap(sellToken.address, buyToken.address, data) + const gasPrice = await hre.ethers.provider.getGasPrice() + log(`gas price ${formatUnits(gasPrice, "gwei")}`) + + const maxFeeScaled = simpleToExactAmount(maxFee, sellToken.decimals) + if (maxFeeScaled.gt(0) && sellOrder.fromAssetFeeAmount.gt(maxFeeScaled)) { + throw Error(`Fee ${formatUnits(sellOrder.fromAssetFeeAmount, sellToken.decimals)} is greater than maxFee ${maxFee}`) + } + + if (!readonly) { + // Initiate the order and sign + const data = encodeInitiateSwap(sellOrder.orderUid, transfer) + const tx = await liquidatorContract.initiateSwap(sellToken.address, buyToken.address, data) - await logTxDetails(tx, `liquidator initiateSwap of ${sellToken.symbol} to ${buyToken.symbol}`) + await logTxDetails(tx, `liquidator initiateSwap of ${sellToken.symbol} to ${buyToken.symbol}`) + } }) task("liq-init-swap").setAction(async (_, __, runSuper) => { await runSuper() @@ -232,21 +257,36 @@ task("liq-sync-swap").setAction(async (_, __, runSuper) => { subtask("liq-donate-tokens", "Donate purchased tokens to vaults") .addParam("vaults", "Comma separated vault symbols or addresses", undefined, types.string) + .addParam("rewards", "Comma separated symbols or addresses of the reward tokens", undefined, types.string) + .addOptionalParam("purchase", "Symbol or address of the purchased token", "DAI", types.string) .addOptionalParam("liquidator", "Liquidator address override", "LiquidatorV2", types.string) .addOptionalParam("speed", "Defender Relayer speed param: 'safeLow' | 'average' | 'fast' | 'fastest'", "fast", types.string) .setAction(async (taskArgs, hre) => { - const { liquidator, speed, vaults } = taskArgs + const { liquidator, speed, rewards, purchase, vaults } = taskArgs const chain = getChain(hre) const signer = await getSigner(hre, speed) const liquidatorAddress = resolveAddress(liquidator, chain) const liquidatorContract = Liquidator__factory.connect(liquidatorAddress, signer) - const vaultsAddress = await resolveMultipleAddress(chain, vaults) - // TODO need option to restrict the reward and purchase tokens - const { rewardTokens, purchaseTokens, vaults: vaultAddresses } = await buildDonateTokensInput(signer, vaultsAddress) - log(`rewardTokens: ${rewardTokens}`) - log(`purchaseTokens: ${purchaseTokens}`) - log(`purchaseVaultAddresses: ${vaultAddresses}`) + const rewardAddresses = await resolveMultipleAddress(chain, rewards) + const vaultAddresses = await resolveMultipleAddress(chain, vaults) + const purchaseToken = await resolveAddress(purchase, chain) + + const rewardTokens = [] + const vaultTokens = [] + const purchaseTokens = [] + // For each vault + vaultAddresses.forEach((vaultAddress) => { + // For each reward token + rewardAddresses.forEach((rewardAddress) => { + rewardTokens.push(rewardAddress) + vaultTokens.push(vaultAddress) + purchaseTokens.push(purchaseToken) + }) + }) + log(`reward tokens ${rewardTokens}`) + log(`vaults ${vaultTokens}`) + log(`purchase tokens ${purchaseTokens}`) const tx = await liquidatorContract.donateTokens(rewardTokens, purchaseTokens, vaultAddresses) await logTxDetails(tx, `liquidator.donateTokens(${rewardTokens}, ${purchaseTokens}, ${vaultAddresses})`) diff --git a/tasks/metaVaultManage.ts b/tasks/metaVaultManage.ts index a778607..f4e7ab4 100644 --- a/tasks/metaVaultManage.ts +++ b/tasks/metaVaultManage.ts @@ -1,10 +1,13 @@ +import { impersonate } from "@utils/fork" import { simpleToExactAmount } from "@utils/math" import { formatUnits } from "ethers/lib/utils" import { subtask, task, types } from "hardhat/config" import { AssetPerShareAbstractVault__factory, FeeAdminAbstractVault__factory, + IERC20__factory, IERC20Metadata__factory, + IERC4626Vault__factory, PerfFeeAbstractVault__factory, PeriodicAllocationAbstractVault__factory, SameAssetUnderlyingsAbstractVault__factory, @@ -12,7 +15,7 @@ import { import { logTxDetails } from "./utils/deploy-utils" import { logger } from "./utils/logger" -import { getChain, resolveAddress } from "./utils/networkAddressFactory" +import { getChain, resolveAddress, resolveAssetToken, resolveVaultToken } from "./utils/networkAddressFactory" import { getSigner } from "./utils/signerFactory" const log = logger("task:mv") @@ -60,8 +63,8 @@ subtask("mv-charge-perf-fee", "Vault Manager charges a performance fee since the const receipt = await tx.wait() const prefFeeEvent = receipt.events?.find((e) => e.event === "PerformanceFee") if (prefFeeEvent) { - log(`${prefFeeEvent.args.feeReceiver} received ${prefFeeEvent.args.feeShares} shares as a fee`) - log(`Fee assets/share updated to ${formatUnits(prefFeeEvent.args.assetsPerShare)}`) + log(`${prefFeeEvent.args.feeReceiver} received ${formatUnits(prefFeeEvent.args.feeShares)} shares as a fee`) + log(`Fee assets/share updated to ${formatUnits(prefFeeEvent.args.assetsPerShare, 26)}`) } else { log("No performance fee was charged") } @@ -242,7 +245,7 @@ task("mv-update-asset-per-share").setAction(async (_, __, runSuper) => { }) subtask("mv-settle", "Vault Manager invests the assets sitting in the vault to underlying vaults.") - .addParam("vault", "Symbol or address of the meta vault.", undefined, types.string) + .addParam("vault", "Symbol or address of the meta vault.", "mv3CRV-CVX", types.string) .addParam( "underlyings", 'json array. eg [{"vaultIndex": 3, "assets": 10000},{"vaultIndex": 4, "assets": 20000}]', @@ -309,4 +312,92 @@ task("mv-rebalance").setAction(async (_, __, runSuper) => { await runSuper() }) +subtask("vault-slippage", "Slippage from a deposit and full redeem") + .addParam("vault", "Vault symbol or address. eg mvDAI-3PCV or vcx3CRV-FRAX, ", undefined, types.string) + .addParam("amount", "Amount as vault shares to burn.", undefined, types.float) + .addOptionalParam("approve", "Will approve the vault to transfer the assets", true, types.boolean) + .addOptionalParam("metaVault", "Symbol or address of the meta vault.", "mv3CRV-CVX", types.string) + .addOptionalParam("settle", "Settle the meta vault", false, types.boolean) + .setAction(async (taskArgs, hre) => { + const { approve, amount, vault, metaVault, settle, speed } = taskArgs + + if (hre?.network.name === "mainnet") throw Error("Slippage calculation not supported on mainnet. Use a fork instead") + + const chain = getChain(hre) + const signer = await getSigner(hre, speed) + const signerAddress = await signer.getAddress() + + const vaultToken = await resolveVaultToken(signer, chain, vault) + const vaultContract = IERC4626Vault__factory.connect(vaultToken.address, signer) + const assetToken = await resolveAssetToken(signer, chain, vaultToken.assetSymbol) + const assetsScaled = simpleToExactAmount(amount, assetToken.decimals) + + if (approve) { + const assetContract = IERC20__factory.connect(assetToken.address, signer) + const approveTx = await assetContract.approve(vaultToken.address, assetsScaled) + await logTxDetails( + approveTx, + `approve ${vaultToken.symbol} vault to transfer ${formatUnits(assetsScaled, assetToken.decimals)} ${ + assetToken.symbol + } assets`, + ) + } + + // Deposit + const tx = await vaultContract.deposit(assetsScaled, signerAddress) + const receipt = await tx.wait() + const depositEvent = receipt.events.find((e) => e.event == "Deposit" && e.address == vaultToken.address) + const shares = depositEvent.args.shares + log(`Deposit ${amount} ${assetToken.symbol} for ${formatUnits(shares, vaultToken.decimals)} ${vaultToken.symbol} shares`) + + if (settle) { + const metaVaultAddress = await resolveAddress(metaVault, chain) + const metaVaultContract = PeriodicAllocationAbstractVault__factory.connect(metaVaultAddress, signer) + + const threeCrvAddress = await resolveAddress("3Crv", chain) + const threeCrvContract = IERC20Metadata__factory.connect(threeCrvAddress, signer) + const threeCrvInMetaVault = await threeCrvContract.balanceOf(metaVaultAddress) + + const settlements = [ + { + vaultIndex: 0, + assets: threeCrvInMetaVault.div(3), + }, + { + vaultIndex: 1, + assets: threeCrvInMetaVault.div(3), + }, + { + vaultIndex: 2, + assets: threeCrvInMetaVault.div(3), + }, + ] + + const vaultManagerSigner = await impersonate(resolveAddress("VaultManager", chain)) + const settleTx = await metaVaultContract.connect(vaultManagerSigner).settle(settlements) + await logTxDetails(settleTx, `${signerAddress} settle ${vault} meta vault`) + } + + // Redeem + const redeemedAssets = await vaultContract.callStatic.redeem(shares, signerAddress, signerAddress) + log( + `Redeemed ${formatUnits(redeemedAssets, assetToken.decimals)} ${assetToken.symbol} from ${formatUnits( + shares, + vaultToken.decimals, + )} ${vaultToken.symbol} shares`, + ) + const diff = redeemedAssets.sub(assetsScaled) + const diffPercentage = diff.mul(1000000).div(assetsScaled) + + log( + `Deposit ${formatUnits(assetsScaled, assetToken.decimals)} ${assetToken.symbol}, redeemed ${formatUnits( + redeemedAssets, + assetToken.decimals, + )} ${assetToken.symbol} diff ${formatUnits(diff, assetToken.decimals)} ${formatUnits(diffPercentage, 4)}%`, + ) + }) +task("vault-slippage").setAction(async (_, __, runSuper) => { + await runSuper() +}) + module.exports = {} diff --git a/tasks/proxyAdmin.ts b/tasks/proxyAdmin.ts index 3d48141..d8fcd0f 100644 --- a/tasks/proxyAdmin.ts +++ b/tasks/proxyAdmin.ts @@ -116,7 +116,7 @@ task("proxy-upgrades", "Lists all proxy implementation changes") console.log(`${assetToken.symbol} proxy ${assetToken.address}`) // eslint-disable-next-line @typescript-eslint/no-explicit-any logs.forEach((eventLog: any) => { - console.log(`Upgraded at block ${eventLog.blockNumber} to ${eventLog.args.implementation} in tx in ${eventLog.blockHash}`) + console.log(`Upgraded at block ${eventLog.blockNumber} to ${eventLog.args.implementation} in tx ${eventLog.blockHash}`) }) }) diff --git a/tasks/utils/blocks.ts b/tasks/utils/blocks.ts index a9f3e40..fc1ad63 100644 --- a/tasks/utils/blocks.ts +++ b/tasks/utils/blocks.ts @@ -1,6 +1,7 @@ export interface BlockInfo { blockNumber: number blockTime: Date + blockTimestamp: number } export interface BlockRange { @@ -11,12 +12,13 @@ export interface BlockRange { // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types export const getBlock = async (ethers, _blockNumber?: number | string): Promise => { const blockNumber = _blockNumber || (await ethers.provider.getBlockNumber()) - const toBlock = await ethers.provider.getBlock(blockNumber) - const blockTime = new Date(toBlock.timestamp * 1000) + const block = await ethers.provider.getBlock(blockNumber) + const blockTime = new Date(block.timestamp * 1000) return { blockNumber, blockTime, + blockTimestamp: block.timestamp, } } diff --git a/tasks/utils/taskUtils.ts b/tasks/utils/taskUtils.ts index 9118f55..0d1d461 100644 --- a/tasks/utils/taskUtils.ts +++ b/tasks/utils/taskUtils.ts @@ -20,7 +20,7 @@ export const params = { if (!isValid) { throw new HardhatError(ERRORS.ARGUMENTS.INVALID_VALUE_FOR_TYPE, { - value, + value: value as unknown as string, name: argName, type: "address", }) @@ -35,7 +35,7 @@ export const params = { if (!isValid) { throw new HardhatError(ERRORS.ARGUMENTS.INVALID_VALUE_FOR_TYPE, { - value, + value: value as unknown as string, name: argName, type: "address[]", }) diff --git a/tasks/vault.ts b/tasks/vault.ts index b8d75f0..f0df866 100644 --- a/tasks/vault.ts +++ b/tasks/vault.ts @@ -48,16 +48,19 @@ subtask("vault-deposit", "Deposit assets into a vault from the signer's account" const assets = simpleToExactAmount(amount, assetToken.decimals) if (approve) { - const asset = IERC20__factory.connect(assetToken.address, signer) - const approveTx = await asset.approve(vaultToken.address, assets) - await logTxDetails(approveTx, `approve ${vaultToken.symbol} vault to transfer ${vaultToken.assetSymbol} assets`) + const assetContract = IERC20__factory.connect(assetToken.address, signer) + const approveTx = await assetContract.approve(vaultToken.address, assets) + await logTxDetails( + approveTx, + `approve ${vaultToken.symbol} vault to transfer ${formatUnits(assets, assetToken.decimals)} ${assetToken.symbol} assets`, + ) } const tx = await vaultContract.deposit(assets, receiverAddress) await logTxDetails( tx, - `${signerAddress} deposited ${formatUnits(assets, assetToken.decimals)} ${assetToken.assetSymbol} into ${ + `${signerAddress} deposited ${formatUnits(assets, assetToken.decimals)} ${assetToken.symbol} into ${ vaultToken.symbol } vault minting to ${receiverAddress}`, ) @@ -96,9 +99,12 @@ subtask("vault-mint", "Mint vault shares by depositing assets from the signer's if (approve) { const assets = await vaultContract.previewMint(shares) - const asset = IERC20__factory.connect(assetToken.address, signer) - const approveTx = await asset.approve(vaultToken.address, assets) - await logTxDetails(approveTx, `approve ${vaultToken.symbol} vault to transfer ${vaultToken.assetSymbol} assets`) + const assetContract = IERC20__factory.connect(assetToken.address, signer) + const approveTx = await assetContract.approve(vaultToken.address, assets) + await logTxDetails( + approveTx, + `approve ${vaultToken.symbol} vault to transfer ${formatUnits(assets, assetToken.decimals)} ${assetToken.symbol} assets`, + ) } const tx = await vaultContract.mint(shares, receiverAddress) @@ -162,6 +168,38 @@ task("vault-withdraw").setAction(async (_, __, runSuper) => { await runSuper() }) +subtask("vault-max-withdraw", "Owner's max asset withdraw from a vault") + .addParam("vault", "Vault symbol or address. eg mvDAI-3PCV or vcx3CRV-FRAX, ", undefined, types.string) + .addOptionalParam( + "owner", + "Address or contract name of the vault share's owner. Default to the signer's address", + undefined, + types.string, + ) + .addOptionalParam("block", "Block number. (default: current block)", 0, types.int) + .setAction(async (taskArgs, hre) => { + const { block, vault, owner, speed } = taskArgs + + const chain = getChain(hre) + const signer = await getSigner(hre, speed) + const signerAddress = await signer.getAddress() + + const blk = await getBlock(hre.ethers, block) + + const vaultToken = await resolveVaultToken(signer, chain, vault) + const vaultContract = IERC4626Vault__factory.connect(vaultToken.address, signer) + const assetToken = await resolveAssetToken(signer, chain, vaultToken.assetSymbol) + + const ownerAddress = owner ? resolveAddress(owner, chain) : signerAddress + + const assets = await vaultContract.maxWithdraw(ownerAddress, { blockTag: blk.blockNumber }) + + log(`max assets ${formatUnits(assets, assetToken.decimals)} ${assetToken.symbol} for block ${blk.blockNumber} ${blk.blockTime}`) + }) +task("vault-max-withdraw").setAction(async (_, __, runSuper) => { + await runSuper() +}) + subtask("vault-redeem", "Redeem vault shares from a vault") .addParam("vault", "Vault symbol or address. eg mvDAI-3PCV or vcx3CRV-FRAX, ", undefined, types.string) .addParam("amount", "Amount as vault shares to burn.", undefined, types.float) @@ -266,7 +304,7 @@ subtask("vault-snap", "Logs basic vault details") blockTag: blk.blockNumber, }) console.log(`Total Assets : ${usdFormatter(totalAssets, assetToken.decimals)}`) - console.log(`Owner acct : ${owner} ${owner !== ownerAddress ? ownerAddress : ""}`) + console.log(`Owner acct : ${owner || ownerAddress}`) const shareBal = await vaultContract.balanceOf(ownerAddress, { blockTag: blk.blockNumber, }) diff --git a/test-fork/peripheral/Curve/Curve3CrvMetapool.spec.ts b/test-fork/peripheral/Curve/Curve3CrvMetapool.spec.ts index 93bfbf5..9870754 100644 --- a/test-fork/peripheral/Curve/Curve3CrvMetapool.spec.ts +++ b/test-fork/peripheral/Curve/Curve3CrvMetapool.spec.ts @@ -1,35 +1,50 @@ -import { musd3CRV, resolveAddress, ThreeCRV } from "@tasks/utils" +import { mUSD, musd3CRV, resolveAddress, ThreeCRV } from "@tasks/utils" import { logger } from "@tasks/utils/logger" -import { impersonateAccount } from "@utils/fork" +import { ZERO, ZERO_ADDRESS } from "@utils/constants" +import { impersonate, impersonateAccount } from "@utils/fork" import { basisPointDiff, BN, simpleToExactAmount } from "@utils/math" import { expect } from "chai" import { ethers } from "ethers" +import { formatUnits } from "ethers/lib/utils" import * as hre from "hardhat" -import { ICurve3Pool__factory, ICurveMetapool__factory, IERC20__factory, IERC20Metadata__factory } from "types/generated" - +import { + Convex3CrvLiquidatorVault__factory, + Curve3CrvMetapoolCalculatorLibrary__factory, + ICurve3Pool__factory, + ICurveMetapool__factory, + IERC20__factory, + IERC20Metadata__factory, + MockERC20__factory, +} from "types/generated" + +import type { Signer } from "ethers" import type { Account } from "types/common" -import type { ICurve3Pool, ICurveMetapool, IERC20 } from "types/generated" +import type { Curve3CrvMetapoolCalculatorLibrary, ICurve3Pool, ICurveMetapool, IERC20, MockERC20 } from "types/generated" const log = logger("test:CurveMetapoolCalcs") const curveThreePoolAddress = resolveAddress("CurveThreePool") const curveMUSDPoolAddress = resolveAddress("CurveMUSDPool") -const staker1Address = "0xd632f22692fac7611d2aa1c0d552930d43caed3b" +const threeCrvWhaleAddress = "0xd632f22692fac7611d2aa1c0d552930d43caed3b" const mpTokenWhaleAddress = "0xe6e6e25efda5f69687aa9914f8d750c523a1d261" const defaultWithdrawSlippage = 100 const defaultDepositSlippage = 100 +const deployerAddress = resolveAddress("OperationsSigner") describe("Curve musd3Crv Metapool", async () => { - let staker1: Account + let threeCrvWhale: Account let mpTokenWhale: Account let threeCrvToken: IERC20 + let musdToken: IERC20 let threePool: ICurve3Pool let musdMetapool: ICurveMetapool let musd3CrvToken: IERC20 + let deployer: Signer + const { network } = hre - const reset = async (blockNumber: number) => { + const reset = async (blockNumber?: number) => { if (network.name === "hardhat") { await network.provider.request({ method: "hardhat_reset", @@ -43,12 +58,14 @@ describe("Curve musd3Crv Metapool", async () => { ], }) } - staker1 = await impersonateAccount(staker1Address) + threeCrvWhale = await impersonateAccount(threeCrvWhaleAddress) mpTokenWhale = await impersonateAccount(mpTokenWhaleAddress) + deployer = await impersonate(deployerAddress) } const initialise = (owner: Account) => { threeCrvToken = IERC20__factory.connect(ThreeCRV.address, owner.signer) + musdToken = IERC20__factory.connect(mUSD.address, owner.signer) threePool = ICurve3Pool__factory.connect(curveThreePoolAddress, owner.signer) musdMetapool = ICurveMetapool__factory.connect(curveMUSDPoolAddress, owner.signer) musd3CrvToken = IERC20__factory.connect(musd3CRV.address, owner.signer) @@ -200,8 +217,8 @@ describe("Curve musd3Crv Metapool", async () => { }) liquidityAmounts.forEach((liquidityAmount) => { it(`block ${blockNumber}, Deposit amount ${liquidityAmount.toLocaleString("en-US")}`, async () => { - initialise(staker1) - await deposit(musdMetapool, musd3CrvToken, staker1, liquidityAmount) + initialise(threeCrvWhale) + await deposit(musdMetapool, musd3CrvToken, threeCrvWhale, liquidityAmount) }) }) mpTokensAmounts.forEach((mpTokensAmount) => { @@ -218,10 +235,378 @@ describe("Curve musd3Crv Metapool", async () => { }) mpTokensAmounts.forEach((mpTokensAmount) => { it(`block ${blockNumber}, mpTokens demand amount ${mpTokensAmount.toLocaleString("en-US")}`, async () => { - initialise(staker1) - await mint(musdMetapool, musd3CrvToken, staker1, mpTokensAmount) + initialise(threeCrvWhale) + await mint(musdMetapool, musd3CrvToken, threeCrvWhale, mpTokensAmount) }) }) }) }) + + const logPool = async () => { + const musdBalance = await musdMetapool.balances(0) + const threeCrvBalance = await musdMetapool.balances(1) + const totalBalance = musdBalance.add(threeCrvBalance) + log(`mUSD balance: ${formatUnits(musdBalance, mUSD.decimals)} ${formatUnits(musdBalance.mul(10000).div(totalBalance), 2)}%`) + log( + `3Crv balance: ${formatUnits(threeCrvBalance, ThreeCRV.decimals)} ${formatUnits( + threeCrvBalance.mul(10000).div(totalBalance), + 2, + )}%`, + ) + log(`total supply ${formatUnits(await musd3CrvToken.totalSupply())}`) + log(`virtual price ${formatUnits(await musdMetapool.get_virtual_price())}\n`) + } + describe(`Add 3Crv liquidity worked example`, () => { + let metapoolLibrary: Curve3CrvMetapoolCalculatorLibrary + let musdBalanceBefore: BN + let threeCrvBalanceBefore: BN + const addAmount = simpleToExactAmount(100000, 18) + + beforeEach(async () => { + await reset(15860000) + const metapoolLibAddress = resolveAddress("Curve3CrvMetapoolCalculatorLibrary") + metapoolLibrary = Curve3CrvMetapoolCalculatorLibrary__factory.connect(metapoolLibAddress, mpTokenWhale.signer) + + initialise(threeCrvWhale) + + musdBalanceBefore = await musdMetapool.balances(0) + threeCrvBalanceBefore = await musdMetapool.balances(1) + + log(`mUSD balance before: ${formatUnits(musdBalanceBefore, mUSD.decimals)}`) + log(`3Crv balance before: ${formatUnits(threeCrvBalanceBefore, ThreeCRV.decimals)}`) + }) + it("Less 3Crv", async () => { + const musdBalanced = simpleToExactAmount(2000000) // 2m + const threeCrvBalanced = simpleToExactAmount(6000000) // 6m + + const musdWithdrawResult = await metapoolLibrary.calcWithdraw( + musdMetapool.address, + musd3CrvToken.address, + // need to fudge the number a little bit as fees are also taken out + musdBalanceBefore.sub(musdBalanced).sub(simpleToExactAmount(437)), + 0, + ) + + await musdMetapool.connect(mpTokenWhale.signer).remove_liquidity_one_coin(musdWithdrawResult.burnAmount_, 0, 0) + + const threeCrvWithdrawResult = await metapoolLibrary.calcWithdraw( + musdMetapool.address, + musd3CrvToken.address, + // need to fudge the number a little bit as fees are also taken out + threeCrvBalanceBefore.sub(threeCrvBalanced).sub(simpleToExactAmount(38)), + 1, + ) + + await musdMetapool.connect(mpTokenWhale.signer).remove_liquidity_one_coin(threeCrvWithdrawResult.burnAmount_, 1, 0) + + log("balanced pool") + await logPool() + + await threeCrvToken.connect(threeCrvWhale.signer).approve(musdMetapool.address, addAmount) + const lpTokens = await musdMetapool.connect(threeCrvWhale.signer).callStatic.add_liquidity([0, addAmount], 0) + const virtualPrice = await musdMetapool.get_virtual_price() + const dollarValue = virtualPrice.mul(lpTokens) + log( + `Received ${formatUnits(lpTokens)} lp tokens worth ${formatUnits(dollarValue, 36)} USD from adding ${formatUnits( + addAmount, + )} 3Crv liquidity`, + ) + }) + it("More 3Crv", async () => { + const musdBalanced = simpleToExactAmount(6000000) // 6m + const threeCrvBalanced = simpleToExactAmount(2000000) // 2m + + const musdWithdrawResult = await metapoolLibrary.calcWithdraw( + musdMetapool.address, + musd3CrvToken.address, + // need to fudge the number a little bit as fees are also taken out + musdBalanceBefore.sub(musdBalanced).sub(simpleToExactAmount(13)), + 0, + ) + + await musdMetapool.connect(mpTokenWhale.signer).remove_liquidity_one_coin(musdWithdrawResult.burnAmount_, 0, 0) + + const threeCrvWithdrawResult = await metapoolLibrary.calcWithdraw( + musdMetapool.address, + musd3CrvToken.address, + // need to fudge the number a little bit as fees are also taken out + threeCrvBalanceBefore.sub(threeCrvBalanced).sub(simpleToExactAmount(450)), + 1, + ) + + await musdMetapool.connect(mpTokenWhale.signer).remove_liquidity_one_coin(threeCrvWithdrawResult.burnAmount_, 1, 0) + + log("balanced pool") + await logPool() + + await threeCrvToken.connect(threeCrvWhale.signer).approve(musdMetapool.address, addAmount) + const lpTokens = await musdMetapool.connect(threeCrvWhale.signer).callStatic.add_liquidity([0, addAmount], 0) + const virtualPrice = await musdMetapool.get_virtual_price() + const dollarValue = virtualPrice.mul(lpTokens) + log( + `Received ${formatUnits(lpTokens)} lp tokens worth ${formatUnits(dollarValue, 36)} USD from adding ${formatUnits( + addAmount, + )} 3Crv liquidity`, + ) + }) + }) + describe(`Sandwich attack worked example`, () => { + let metapoolLibrary: Curve3CrvMetapoolCalculatorLibrary + const threeCrvBalanced = simpleToExactAmount(6000000) // 6m + const attackerAdd3CrvBefore = simpleToExactAmount(50000000) // 50m + const attackerRemoveLpBefore = simpleToExactAmount(6500000) //6.5m + const victim3CrvBefore = simpleToExactAmount(100000) // 100k + let victimBalancedLpTokens: BN + let victimDollarValueBefore: BN + let otherLpTokens: BN + + beforeEach(async () => { + await reset(15860000) + const metapoolLibAddress = resolveAddress("Curve3CrvMetapoolCalculatorLibrary") + metapoolLibrary = Curve3CrvMetapoolCalculatorLibrary__factory.connect(metapoolLibAddress, mpTokenWhale.signer) + + initialise(threeCrvWhale) + + const musdBalanceBefore = await musdMetapool.balances(0) + const threeCrvBalanceBefore = await musdMetapool.balances(1) + + log(`mUSD balance before: ${formatUnits(musdBalanceBefore, mUSD.decimals)}`) + log(`3Crv balance before: ${formatUnits(threeCrvBalanceBefore, ThreeCRV.decimals)}`) + + const musdWithdrawResult = await metapoolLibrary.calcWithdraw( + musdMetapool.address, + musd3CrvToken.address, + // need to fudge the number a little bit as fees are also taken out + musdBalanceBefore.sub(threeCrvBalanced).sub(simpleToExactAmount(13)), + 0, + ) + + await musdMetapool.connect(mpTokenWhale.signer).remove_liquidity_one_coin(musdWithdrawResult.burnAmount_, 0, 0) + + const threeCrvWithdrawResult = await metapoolLibrary.calcWithdraw( + musdMetapool.address, + musd3CrvToken.address, + // need to fudge the number a little bit as fees are also taken out + threeCrvBalanceBefore.sub(threeCrvBalanced).sub(simpleToExactAmount(78)), + 1, + ) + + await musdMetapool.connect(mpTokenWhale.signer).remove_liquidity_one_coin(threeCrvWithdrawResult.burnAmount_, 1, 0) + + log("balanced pool") + await logPool() + + log(`lp whale balance before: ${formatUnits(await musd3CrvToken.balanceOf(mpTokenWhale.address))}`) + + const virtualPrice = await musdMetapool.get_virtual_price() + const attackerLpValue = virtualPrice.mul(attackerRemoveLpBefore) + log(`attacker lp tokens ${formatUnits(attackerRemoveLpBefore, 18)} worth ${formatUnits(attackerLpValue, 36)} USD`) + const totalLpTokens = await musd3CrvToken.totalSupply() + otherLpTokens = totalLpTokens.sub(attackerRemoveLpBefore) + const otherLpValue = virtualPrice.mul(otherLpTokens) + log(`other lp tokens ${formatUnits(otherLpTokens, 18)} worth ${formatUnits(otherLpValue, 36)} USD`) + }) + it("add liquidity to balanced pool", async () => { + await threeCrvToken.connect(threeCrvWhale.signer).approve(musdMetapool.address, victim3CrvBefore) + victimBalancedLpTokens = await musdMetapool.connect(threeCrvWhale.signer).callStatic.add_liquidity([0, victim3CrvBefore], 0) + const virtualPrice = await musdMetapool.get_virtual_price() + victimDollarValueBefore = virtualPrice.mul(victimBalancedLpTokens) + log( + `Received ${formatUnits(victimBalancedLpTokens)} lp tokens worth ${formatUnits( + victimDollarValueBefore, + 36, + )} USD from adding ${formatUnits(victim3CrvBefore)} 3Crv liquidity`, + ) + }) + describe.skip("add liquidity to imbalance pool", () => { + let attackerLpTokens: BN + beforeEach(async () => { + // Attacker's first tx adds 3Crv to the pool + await threeCrvToken.connect(threeCrvWhale.signer).approve(musdMetapool.address, attackerAdd3CrvBefore) + // static call to easily get the lp tokens + attackerLpTokens = await musdMetapool.connect(threeCrvWhale.signer).callStatic.add_liquidity([0, attackerAdd3CrvBefore], 0) + await musdMetapool.connect(threeCrvWhale.signer).add_liquidity([0, attackerAdd3CrvBefore], 0) + + log("Attacker added 3Crv to the pool") + await logPool() + }) + it("sandwich attack", async () => { + // victim tx is second tx that adds 3Crv to the pool + await threeCrvToken.connect(threeCrvWhale.signer).approve(musdMetapool.address, victim3CrvBefore) + // static call to easily get the lp tokens + const victimImbalancedLpTokens = await musdMetapool + .connect(threeCrvWhale.signer) + .callStatic.add_liquidity([0, victim3CrvBefore], 0) + await musdMetapool.connect(threeCrvWhale.signer).add_liquidity([0, victim3CrvBefore], 0) + const victimLpTokenDiff = victimImbalancedLpTokens.sub(victimBalancedLpTokens) + const victimLpTokenDiffPercent = victimLpTokenDiff.mul(100000000).div(victimBalancedLpTokens) + log( + `victim got ${formatUnits(victimImbalancedLpTokens)} diff ${formatUnits(victimLpTokenDiff)} ${formatUnits( + victimLpTokenDiffPercent, + 6, + )}% musd3CRV lp tokens`, + ) + const virtualPrice = await musdMetapool.get_virtual_price() + const victimDollarValueAfter = virtualPrice.mul(victimImbalancedLpTokens) + const victimDollarValueDiff = victimDollarValueAfter.sub(victimDollarValueBefore) + const victimDollarLpTokenDiffPercent = victimDollarValueDiff.mul(100000000).div(victimDollarValueBefore) + log( + `victim USD value of lp tokens ${formatUnits(victimDollarValueAfter)} diff ${formatUnits( + victimDollarValueDiff, + )} ${formatUnits(victimDollarLpTokenDiffPercent, 6)}%`, + ) + + // third tx the attacker redeems their extra metapool lp tokens for 3Crv + const attacker3CrvAfter = await musdMetapool + .connect(threeCrvWhale.signer) + .callStatic.remove_liquidity_one_coin(attackerLpTokens, 1, 0) + await musdMetapool.connect(threeCrvWhale.signer).remove_liquidity_one_coin(attackerLpTokens, 1, 0) + log("After 3rd tx") + const attacker3CrvDiff = attacker3CrvAfter.sub(attackerAdd3CrvBefore) + const attacker3CrvDiffPercentage = attacker3CrvDiff.mul(1000000).div(attackerAdd3CrvBefore) + log( + `attacker withdrew ${formatUnits(attacker3CrvAfter)} 3Crv diff ${formatUnits(attacker3CrvDiff)} ${formatUnits( + attacker3CrvDiffPercentage, + 4, + )}% from ${formatUnits(attackerLpTokens)} lp tokens`, + ) + + await logPool() + + const victim3CrvAfter = await musdMetapool + .connect(mpTokenWhale.signer) + .callStatic.remove_liquidity_one_coin(victimImbalancedLpTokens, 1, 0) + await musdMetapool.connect(mpTokenWhale.signer).remove_liquidity_one_coin(victimImbalancedLpTokens, 1, 0) + const victim3CrvDiff = victim3CrvAfter.sub(victim3CrvBefore) + const victim3CrvDiffPercent = victim3CrvDiff.mul(10000).div(victim3CrvBefore) + log("After victim removes liquidity") + log( + `victim after ${formatUnits(victim3CrvAfter)} 3Crv ${formatUnits(victim3CrvDiff)} ${formatUnits( + victim3CrvDiffPercent, + 2, + )}%`, + ) + await logPool() + }) + it("deposit to mUSD vault should fail due to sandwich protection", async () => { + const vaultAddress = resolveAddress("vcx3CRV-mUSD") + const musdVault = Convex3CrvLiquidatorVault__factory.connect(vaultAddress, threeCrvWhale.signer) + await threeCrvToken.connect(threeCrvWhale.signer).approve(vaultAddress, attackerAdd3CrvBefore) + const tx = musdVault["deposit(uint256,address)"](attackerAdd3CrvBefore, threeCrvWhale.address) + await expect(tx).revertedWith("Slippage screwed you") + }) + }) + it("remove liquidity to imbalanced pool", async () => { + // Attacker's first tx remove mUSD from the pool + // static call to easily get the 3Crv tokens removed + const attackerRemovedMusd = await musdMetapool + .connect(mpTokenWhale.signer) + .callStatic.remove_liquidity_one_coin(attackerRemoveLpBefore, 0, 0) + await musdMetapool.connect(mpTokenWhale.signer).remove_liquidity_one_coin(attackerRemoveLpBefore, 0, 0) + log( + `Attacker removed ${formatUnits(attackerRemovedMusd)} mUSD from the pool using ${formatUnits( + attackerRemoveLpBefore, + )} lp tokens`, + ) + + log("3Crv removed from the pool") + await logPool() + + // victim tx that adds 3Crv to the pool + await threeCrvToken.connect(threeCrvWhale.signer).approve(musdMetapool.address, victim3CrvBefore) + // static call to easily get the lp tokens + const victimImbalancedLpTokens = await musdMetapool + .connect(threeCrvWhale.signer) + .callStatic.add_liquidity([0, victim3CrvBefore], 0) + await musdMetapool.connect(threeCrvWhale.signer).add_liquidity([0, victim3CrvBefore], 0) + const victimLpTokenDiff = victimImbalancedLpTokens.sub(victimBalancedLpTokens) + const victimLpTokenDiffPercent = victimLpTokenDiff.mul(100000000).div(victimBalancedLpTokens) + log( + `victim got ${formatUnits(victimImbalancedLpTokens)} diff ${formatUnits(victimLpTokenDiff)} ${formatUnits( + victimLpTokenDiffPercent, + 6, + )}% musd3CRV lp tokens`, + ) + const virtualPrice = await musdMetapool.get_virtual_price() + const victimDollarValueAfter = virtualPrice.mul(victimImbalancedLpTokens) + const victimDollarValueDiff = victimDollarValueAfter.sub(victimDollarValueBefore) + const victimDollarLpTokenDiffPercent = victimDollarValueDiff.mul(100000000).div(victimDollarValueBefore) + log( + `victim USD value of lp tokens ${formatUnits(victimDollarValueAfter, 36)} diff ${formatUnits( + victimDollarValueDiff, + 36, + )} ${formatUnits(victimDollarLpTokenDiffPercent, 6)}%`, + ) + + // third tx the attacker adds the mUSD back to the pool + await musdToken.connect(mpTokenWhale.signer).approve(musdMetapool.address, attackerRemovedMusd) + const attackerLpAfter = await musdMetapool.connect(mpTokenWhale.signer).callStatic.add_liquidity([attackerRemovedMusd, 0], 0) + await musdMetapool.connect(mpTokenWhale.signer).add_liquidity([attackerRemovedMusd, 0], 0) + log("After 3rd tx") + const attackerLpDiff = attackerLpAfter.sub(attackerRemoveLpBefore) + const attackerLpDiffPercentage = attackerLpDiff.mul(1000000).div(attackerRemoveLpBefore) + log( + `attacker received ${formatUnits(attackerLpAfter)} lp tokens (musd3Crv) diff ${formatUnits(attackerLpDiff)} ${formatUnits( + attackerLpDiffPercentage, + 4, + )}% from adding ${formatUnits(attackerRemovedMusd)} mUSD`, + ) + + await logPool() + + const victim3CrvAfter = await musdMetapool + .connect(mpTokenWhale.signer) + .callStatic.remove_liquidity_one_coin(victimImbalancedLpTokens, 1, 0) + await musdMetapool.connect(mpTokenWhale.signer).remove_liquidity_one_coin(victimImbalancedLpTokens, 1, 0) + const victim3CrvDiff = victim3CrvAfter.sub(victim3CrvBefore) + const victim3CrvDiffPercent = victim3CrvDiff.mul(10000).div(victim3CrvBefore) + log("After victim removes liquidity") + log( + `victim after ${formatUnits(victim3CrvAfter)} 3Crv ${formatUnits(victim3CrvDiff)} ${formatUnits( + victim3CrvDiffPercent, + 2, + )}%`, + ) + const otherLpValueAfter = virtualPrice.mul(otherLpTokens) + log(`other lp tokens ${formatUnits(otherLpTokens, 18)} worth ${formatUnits(otherLpValueAfter, 36)} USD`) + await logPool() + }) + }) + describe("Curve3CrvMetapoolCalculatorLibrary", () => { + let metapoolLibrary: Curve3CrvMetapoolCalculatorLibrary + + let emptyPool: MockERC20 + before(async () => { + await reset(15860000) + const metapoolLibAddress = resolveAddress("Curve3CrvMetapoolCalculatorLibrary") + metapoolLibrary = Curve3CrvMetapoolCalculatorLibrary__factory.connect(metapoolLibAddress, mpTokenWhale.signer) + initialise(threeCrvWhale) + + emptyPool = await new MockERC20__factory(deployer).deploy("ERC20 Mock", "ERC20", 18, deployerAddress, 0) + }) + it("fails to calculate deposit in an empty pool", async () => { + await expect(metapoolLibrary.calcDeposit(ZERO_ADDRESS, emptyPool.address, 500, 0)).to.be.revertedWith("empty pool") + }) + it("fails to calculate mint in an empty pool", async () => { + await expect(metapoolLibrary.calcMint(ZERO_ADDRESS, emptyPool.address, 500, 0)).to.be.revertedWith("empty pool") + }) + it("fails to calculate withdraw in an empty pool", async () => { + await expect(metapoolLibrary.calcWithdraw(ZERO_ADDRESS, emptyPool.address, 500, 0)).to.be.revertedWith("empty pool") + }) + it("fails to calculate redeem in an empty pool", async () => { + await expect(metapoolLibrary.calcRedeem(ZERO_ADDRESS, emptyPool.address, 500, 0)).to.be.revertedWith("empty pool") + }) + it("converts with ZERO amounts", async () => { + expect(await metapoolLibrary.convertUsdToBaseLp(ZERO)).to.be.eq(ZERO) + expect(await metapoolLibrary.convertUsdToMetaLp(ZERO_ADDRESS, ZERO)).to.be.eq(ZERO) + expect( + await metapoolLibrary["convertToBaseLp(address,address,uint256,bool)"](ZERO_ADDRESS, ZERO_ADDRESS, ZERO, false), + ).to.be.eq(ZERO) + expect(await metapoolLibrary["convertToBaseLp(address,address,uint256)"](ZERO_ADDRESS, ZERO_ADDRESS, ZERO)).to.be.eq(ZERO) + expect( + await metapoolLibrary["convertToMetaLp(address,address,uint256,bool)"](ZERO_ADDRESS, ZERO_ADDRESS, ZERO, false), + ).to.be.eq(ZERO) + expect(await metapoolLibrary["convertToMetaLp(address,address,uint256)"](ZERO_ADDRESS, ZERO_ADDRESS, ZERO)).to.be.eq(ZERO) + }) + }) }) diff --git a/test-fork/peripheral/Curve/Curve3CrvMetapoolCalculations.spec.ts b/test-fork/peripheral/Curve/Curve3CrvMetapoolCalculations.spec.ts index ab28c78..532cbd8 100644 --- a/test-fork/peripheral/Curve/Curve3CrvMetapoolCalculations.spec.ts +++ b/test-fork/peripheral/Curve/Curve3CrvMetapoolCalculations.spec.ts @@ -1,4 +1,3 @@ -import { config } from "@tasks/deployment/convex3CrvVaults-config" import { DAI, mUSD, musd3CRV, resolveAddress, ThreeCRV, USDC, usdFormatter } from "@tasks/utils" import { logger } from "@tasks/utils/logger" import { impersonateAccount, loadOrExecFixture } from "@utils/fork" @@ -8,7 +7,6 @@ import { ethers } from "ethers" import { formatUnits } from "ethers/lib/utils" import * as hre from "hardhat" import { - Curve3CrvMetapoolCalculator__factory, Curve3CrvMetapoolCalculatorLibrary__factory, ICurve3Pool__factory, ICurveMetapool__factory, @@ -16,7 +14,7 @@ import { } from "types/generated" import type { Account } from "types/common" -import type { Curve3CrvMetapoolCalculator, ICurve3Pool, ICurveMetapool, IERC20 } from "types/generated" +import type { Curve3CrvMetapoolCalculatorLibrary, ICurve3Pool, ICurveMetapool, IERC20 } from "types/generated" const log = logger("test:Curve3CrvMetaCalcs") @@ -47,7 +45,7 @@ describe("Curve 3Crv metapool calculations", async () => { let musd3CrvWhale: Account let threeCrvToken: IERC20 let threePool: ICurve3Pool - let metapoolCalculator: Curve3CrvMetapoolCalculator + let calculatorLibrary: Curve3CrvMetapoolCalculatorLibrary let musdMetapool: ICurveMetapool let musd3CrvToken: IERC20 let musdToken: IERC20 @@ -75,15 +73,7 @@ describe("Curve 3Crv metapool calculations", async () => { musdWhale = await impersonateAccount(musdWhaleAddress) musd3CrvWhale = await impersonateAccount(musd3CrvWhaleAddress) - const calculatorLibrary = await new Curve3CrvMetapoolCalculatorLibrary__factory(staker1.signer).deploy() - const libraryAddresses = { - "contracts/peripheral/Curve/Curve3CrvMetapoolCalculatorLibrary.sol:Curve3CrvMetapoolCalculatorLibrary": - calculatorLibrary.address, - } - metapoolCalculator = await new Curve3CrvMetapoolCalculator__factory(libraryAddresses, staker1.signer).deploy( - config.convex3CrvPools.musd.curveMetapool, - config.convex3CrvPools.musd.curveMetapoolToken, - ) + calculatorLibrary = await new Curve3CrvMetapoolCalculatorLibrary__factory(staker1.signer).deploy() } const initialise = (owner: Account) => { @@ -113,8 +103,10 @@ describe("Curve 3Crv metapool calculations", async () => { const threeCrvBefore = await threeCrvToken.balanceOf(owner.address) const metapoolLpBefore = await metapoolToken.balanceOf(owner.address) - const [metapoolLpCalculated] = await metapoolCalculator.calcDeposit(threeCrvScaled, 1) - const unsignedTx = await metapoolCalculator.connect(owner.signer).populateTransaction.calcDeposit(threeCrvScaled, 1) + const [metapoolLpCalculated] = await calculatorLibrary.calcDeposit(musdMetapool.address, musd3CRV.address, threeCrvScaled, 1) + const unsignedTx = await calculatorLibrary + .connect(owner.signer) + .populateTransaction.calcDeposit(musdMetapool.address, musd3CRV.address, threeCrvScaled, 1) await owner.signer.sendTransaction(unsignedTx) expect(threeCrvBefore, "enough base pool LP tokens (3Crv) to deposit").to.gte(threeCrvScaled) @@ -149,8 +141,10 @@ describe("Curve 3Crv metapool calculations", async () => { const threeCrvBefore = await threeCrvToken.balanceOf(owner.address) const metapoolLpBefore = await metapoolToken.balanceOf(owner.address) - const [metapoolLpCalculated] = await metapoolCalculator.calcWithdraw(threeCrvScaled, 1) - const unsignedTx = await metapoolCalculator.connect(owner.signer).populateTransaction.calcWithdraw(threeCrvScaled, 1) + const [metapoolLpCalculated] = await calculatorLibrary.calcWithdraw(musdMetapool.address, musd3CRV.address, threeCrvScaled, 1) + const unsignedTx = await calculatorLibrary + .connect(owner.signer) + .populateTransaction.calcWithdraw(musdMetapool.address, musd3CRV.address, threeCrvScaled, 1) await owner.signer.sendTransaction(unsignedTx) log(`Metapool LP (musd3Crv) balance ${usdFormatter(await metapoolToken.balanceOf(owner.address))}`) @@ -185,9 +179,11 @@ describe("Curve 3Crv metapool calculations", async () => { const metapoolLpBefore = await metapoolToken.balanceOf(owner.address) // Calculate 3Crv to deposit for the required metapool LP tokens - const [threeCrvCalculated] = await metapoolCalculator.calcMint(lpAmountScaled, 1) + const [threeCrvCalculated] = await calculatorLibrary.calcMint(musdMetapool.address, musd3CRV.address, lpAmountScaled, 1) - const unsignedTx = await metapoolCalculator.connect(owner.signer).populateTransaction.calcMint(lpAmountScaled, 1) + const unsignedTx = await calculatorLibrary + .connect(owner.signer) + .populateTransaction.calcMint(musdMetapool.address, musd3CRV.address, lpAmountScaled, 1) await owner.signer.sendTransaction(unsignedTx) expect(threeCrvBefore, "enough 3Crv tokens to deposit").to.gte(threeCrvCalculated) @@ -224,9 +220,11 @@ describe("Curve 3Crv metapool calculations", async () => { let unsignedTx = await musdMetapool.connect(owner.signer).populateTransaction.calc_withdraw_one_coin(metapoolLpAmountScaled, 1) await owner.signer.sendTransaction(unsignedTx) - const [threeCrvCalculated] = await metapoolCalculator.calcRedeem(metapoolLpAmountScaled, 1) + const [threeCrvCalculated] = await calculatorLibrary.calcRedeem(musdMetapool.address, musd3CRV.address, metapoolLpAmountScaled, 1) - unsignedTx = await metapoolCalculator.connect(owner.signer).populateTransaction.calcRedeem(metapoolLpAmountScaled, 1) + unsignedTx = await calculatorLibrary + .connect(owner.signer) + .populateTransaction.calcRedeem(musdMetapool.address, musd3CRV.address, metapoolLpAmountScaled, 1) await owner.signer.sendTransaction(unsignedTx) expect(metapoolLpBefore, "enough metapool LP tokens (musd3Crv) to withdraw").to.gte(metapoolLpAmountScaled) @@ -310,36 +308,103 @@ describe("Curve 3Crv metapool calculations", async () => { }) it("Convert 100 meta pool lp tokens musd3Crv to base pool lp tokens (3Crv)", async () => { const musd3CrvTokens = simpleToExactAmount(100) - const threeCrvTokens = await metapoolCalculator.convertToBaseLp(musd3CrvTokens, true) + const threeCrvTokens = await calculatorLibrary["convertToBaseLp(address,address,uint256,bool)"]( + musdMetapool.address, + musd3CRV.address, + musd3CrvTokens, + true, + ) log(`${usdFormatter(musd3CrvTokens)} musd3Crv = ${usdFormatter(threeCrvTokens)} threeCrv`) expect(threeCrvTokens, "3Crv < musd3Crv").lt(musd3CrvTokens) await staker1.signer.sendTransaction( - await metapoolCalculator.connect(staker1.signer).populateTransaction.convertToBaseLp(musd3CrvTokens, true), + await calculatorLibrary + .connect(staker1.signer) + .populateTransaction["convertToBaseLp(address,address,uint256,bool)"]( + musdMetapool.address, + musd3CRV.address, + musd3CrvTokens, + true, + ), ) }) it("Convert 100 base pool lp tokens 3Crv to metapool lp tokens (musd3Crv)", async () => { const threeCrvTokens = simpleToExactAmount(100) - const musd3CrvTokens = await metapoolCalculator.convertToMetaLp(threeCrvTokens, true) + const musd3CrvTokens = await calculatorLibrary["convertToMetaLp(address,address,uint256,bool)"]( + musdMetapool.address, + musd3CRV.address, + threeCrvTokens, + true, + ) log(`${usdFormatter(threeCrvTokens)} 3Crv = ${usdFormatter(musd3CrvTokens)} musd3Crv`) expect(threeCrvTokens, "3Crv < musd3Crv").lt(musd3CrvTokens) await staker1.signer.sendTransaction( - await metapoolCalculator.connect(staker1.signer).populateTransaction.convertToMetaLp(threeCrvTokens, true), + await calculatorLibrary + .connect(staker1.signer) + .populateTransaction["convertToMetaLp(address,address,uint256,bool)"]( + musdMetapool.address, + musd3CRV.address, + threeCrvTokens, + true, + ), ) }) it("Metapool virtual prices", async () => { const expectedMetapoolVP = await musdMetapool.get_virtual_price() const expected3PoolVP = await threePool.get_virtual_price() - const [actualMetapoolVP, actual3PoolVP] = await metapoolCalculator.getVirtualPrices(false) + const [actualMetapoolVP, actual3PoolVP] = await calculatorLibrary.getVirtualPrices( + musdMetapool.address, + musd3CRV.address, + false, + ) expect(actualMetapoolVP, "Metapool virtual price").to.eq(expectedMetapoolVP) expect(actual3PoolVP, "3Pool virtual price").to.eq(expected3PoolVP) - await staker1.signer.sendTransaction(await musdMetapool.connect(staker1.signer).populateTransaction.get_virtual_price()) await staker1.signer.sendTransaction( - await metapoolCalculator.connect(staker1.signer).populateTransaction.getVirtualPrices(false), + await calculatorLibrary + .connect(staker1.signer) + .populateTransaction.getVirtualPrices(musdMetapool.address, musd3CRV.address, false), + ) + }) + it("Metapool base virtual price", async () => { + const expected3PoolVP = await threePool.get_virtual_price() + const actual3PoolVP = await calculatorLibrary["getBaseVirtualPrice()"]() + expect(actual3PoolVP, "3Pool virtual price").to.eq(expected3PoolVP) + + await staker1.signer.sendTransaction( + await calculatorLibrary.connect(staker1.signer).populateTransaction["getBaseVirtualPrice()"](), ) }) + it("Metapool cached base virtual price", async () => { + const expected3PoolVP = await threePool.get_virtual_price() + const actual3PoolVP = await calculatorLibrary["getBaseVirtualPrice(address,bool)"](musdMetapool.address, true) + expect(actual3PoolVP, "3Pool virtual price").to.eq(expected3PoolVP) + + await staker1.signer.sendTransaction( + await calculatorLibrary + .connect(staker1.signer) + .populateTransaction["getBaseVirtualPrice(address,bool)"](musdMetapool.address, true), + ) + }) + it("Convert 1000 USD to 3Crv", async () => { + const usdAmount = simpleToExactAmount(1000) + const actual3Pool = await calculatorLibrary.convertUsdToBaseLp(usdAmount) + expect(actual3Pool, "3Pool virtual price").to.lt(usdAmount) + }) + it("Convert 0 USD to 3Crv", async () => { + const actual3Pool = await calculatorLibrary.convertUsdToBaseLp(0) + expect(actual3Pool, "3Pool virtual price").to.eq(0) + }) + it("Convert 1000 USD to Metapool LP", async () => { + const usdAmount = simpleToExactAmount(1000) + const actualMetapoolLP = await calculatorLibrary.convertUsdToMetaLp(musdMetapool.address, usdAmount) + expect(actualMetapoolLP, "3Pool virtual price").to.lt(usdAmount) + }) + it("Convert 0 USD to Metapool LP", async () => { + const actualMetapoolLP = await calculatorLibrary.convertUsdToMetaLp(musdMetapool.address, 0) + expect(actualMetapoolLP, "3Pool virtual price").to.eq(0) + }) } const usdcOverweight3Pool = async () => { // Add 600m USDC @@ -412,7 +477,7 @@ describe("Curve 3Crv metapool calculations", async () => { const threeCrvTokens = simpleToExactAmount(1408215, 16) const expectedLlpTokens = BN.from("14133074249861806636919") - const [calculatedLp] = await metapoolCalculator.calcDeposit(threeCrvTokens, 1) + const [calculatedLp] = await calculatorLibrary.calcDeposit(musdMetapool.address, musd3CRV.address, threeCrvTokens, 1) expect(threeCrvBefore, "enough LP tokens (3Crv) to deposit").to.gte(threeCrvTokens) diff --git a/test-fork/peripheral/Curve/Curve3PoolCalculations.spec.ts b/test-fork/peripheral/Curve/Curve3PoolCalculations.spec.ts index 6c50d1c..4c0581e 100644 --- a/test-fork/peripheral/Curve/Curve3PoolCalculations.spec.ts +++ b/test-fork/peripheral/Curve/Curve3PoolCalculations.spec.ts @@ -6,11 +6,11 @@ import { expect } from "chai" import { BigNumber, ethers } from "ethers" import { formatUnits } from "ethers/lib/utils" import * as hre from "hardhat" -import { Curve3PoolCalculator__factory, Curve3PoolCalculatorLibrary__factory, ICurve3Pool__factory, IERC20__factory } from "types/generated" +import { Curve3PoolCalculatorLibrary__factory, ICurve3Pool__factory, IERC20__factory } from "types/generated" import type { BlockTag } from "@nomicfoundation/hardhat-network-helpers/dist/src/types" import type { Account } from "types/common" -import type { Curve3PoolCalculator, ICurve3Pool, IERC20 } from "types/generated" +import type { Curve3PoolCalculatorLibrary, ICurve3Pool, IERC20 } from "types/generated" const log = logger("test:Curve3PoolCalcs") @@ -33,7 +33,7 @@ describe("Curve 3Pool calculations", async () => { let threeCrvWhale2: Account let threeCrvToken: IERC20 let threePool: ICurve3Pool - let poolCalculator: Curve3PoolCalculator + let calculatorLibrary: Curve3PoolCalculatorLibrary let usdcToken: IERC20 let daiToken: IERC20 let usdtToken: IERC20 @@ -57,11 +57,7 @@ describe("Curve 3Pool calculations", async () => { threeCrvWhale1 = await impersonateAccount(threeCrvWhaleAddress1) threeCrvWhale2 = await impersonateAccount(threeCrvWhaleAddress2) - const threePoolCalculatorLibrary = await new Curve3PoolCalculatorLibrary__factory(staker1.signer).deploy() - const curve3PoolCalculatorLibraryAddresses = { - "contracts/peripheral/Curve/Curve3PoolCalculatorLibrary.sol:Curve3PoolCalculatorLibrary": threePoolCalculatorLibrary.address, - } - poolCalculator = await new Curve3PoolCalculator__factory(curve3PoolCalculatorLibraryAddresses, staker1.signer).deploy() + calculatorLibrary = await new Curve3PoolCalculatorLibrary__factory(staker1.signer).deploy() } const initialise = (owner: Account) => { @@ -96,8 +92,8 @@ describe("Curve 3Pool calculations", async () => { await threePool.connect(owner.signer).populateTransaction.calc_token_amount([0, usdcScaled, 0], true), ) - const [lpCalculated] = await poolCalculator.calcDeposit(usdcScaled, 1) - await owner.signer.sendTransaction(await poolCalculator.connect(owner.signer).populateTransaction.calcDeposit(usdcScaled, 1)) + const [lpCalculated] = await calculatorLibrary.calcDeposit(usdcScaled, 1) + await owner.signer.sendTransaction(await calculatorLibrary.connect(owner.signer).populateTransaction.calcDeposit(usdcScaled, 1)) expect(usdcBefore, "enough USDC tokens to deposit").to.gte(usdcScaled) @@ -136,8 +132,8 @@ describe("Curve 3Pool calculations", async () => { await threePool.connect(owner.signer).populateTransaction.calc_token_amount([0, usdcScaled, 0], false), ) - const [lpCalculated] = await poolCalculator.calcWithdraw(usdcScaled, 1) - await owner.signer.sendTransaction(await poolCalculator.connect(owner.signer).populateTransaction.calcWithdraw(usdcScaled, 1)) + const [lpCalculated] = await calculatorLibrary.calcWithdraw(usdcScaled, 1) + await owner.signer.sendTransaction(await calculatorLibrary.connect(owner.signer).populateTransaction.calcWithdraw(usdcScaled, 1)) expect(lpBefore, "enough LP tokens (3Crv) to withdraw").to.gte(lpCalculated) @@ -171,8 +167,8 @@ describe("Curve 3Pool calculations", async () => { const lpBefore = await threeCrvToken.balanceOf(owner.address) // Calculate USDC assets for required LP tokens - const [usdcCalculated] = await poolCalculator.calcMint(lpAmountScaled, 1) - await owner.signer.sendTransaction(await poolCalculator.connect(owner.signer).populateTransaction.calcMint(lpAmountScaled, 1)) + const [usdcCalculated] = await calculatorLibrary.calcMint(lpAmountScaled, 1) + await owner.signer.sendTransaction(await calculatorLibrary.connect(owner.signer).populateTransaction.calcMint(lpAmountScaled, 1)) expect(usdcBefore, "enough USDC tokens to deposit").to.gte(usdcCalculated) @@ -208,8 +204,8 @@ describe("Curve 3Pool calculations", async () => { await threePool.connect(owner.signer).populateTransaction.calc_withdraw_one_coin(lpAmountScaled, 1), ) - const [usdcCalculated] = await poolCalculator.calcRedeem(lpAmountScaled, 1) - await owner.signer.sendTransaction(await poolCalculator.connect(owner.signer).populateTransaction.calcRedeem(lpAmountScaled, 1)) + const [usdcCalculated] = await calculatorLibrary.calcRedeem(lpAmountScaled, 1) + await owner.signer.sendTransaction(await calculatorLibrary.connect(owner.signer).populateTransaction.calcRedeem(lpAmountScaled, 1)) expect(lpBefore, "enough LP tokens (3Crv) to withdraw").to.gte(lpAmountScaled) @@ -275,11 +271,11 @@ describe("Curve 3Pool calculations", async () => { }) it("3Pool virtual prices", async () => { const expectedVirtualPrice = await threePool.get_virtual_price() - expect(await poolCalculator.getVirtualPrice(), "virtual price").to.eq(expectedVirtualPrice) + expect(await calculatorLibrary.getVirtualPrice(), "virtual price").to.eq(expectedVirtualPrice) // Get the gas costs await staker1.signer.sendTransaction(await threePool.connect(staker1.signer).populateTransaction.get_virtual_price()) - await staker1.signer.sendTransaction(await poolCalculator.connect(staker1.signer).populateTransaction.getVirtualPrice()) + await staker1.signer.sendTransaction(await calculatorLibrary.connect(staker1.signer).populateTransaction.getVirtualPrice()) }) } const usdcOverweight3Pool = async () => { @@ -323,8 +319,8 @@ describe("Curve 3Pool calculations", async () => { const daiBefore = await daiToken.balanceOf(staker1.address) const lpBefore = await threeCrvToken.balanceOf(staker1.address) - const [calculatedLp] = await poolCalculator.calcDeposit(daiTokens, 0) - await staker1.signer.sendTransaction(await poolCalculator.populateTransaction.calcDeposit(daiTokens, 0)) + const [calculatedLp] = await calculatorLibrary.calcDeposit(daiTokens, 0) + await staker1.signer.sendTransaction(await calculatorLibrary.populateTransaction.calcDeposit(daiTokens, 0)) await daiToken.connect(staker1.signer).approve(threePool.address, daiTokens) await threePool.connect(staker1.signer).add_liquidity([daiTokens, 0, 0], 0) @@ -341,8 +337,8 @@ describe("Curve 3Pool calculations", async () => { const daiBefore = await daiToken.balanceOf(staker1.address) const lpBefore = await threeCrvToken.balanceOf(staker1.address) - const [daiCalculated] = await poolCalculator.calcMint(requiredLpTokens, 0) - await staker1.signer.sendTransaction(await poolCalculator.populateTransaction.calcMint(requiredLpTokens, 0)) + const [daiCalculated] = await calculatorLibrary.calcMint(requiredLpTokens, 0) + await staker1.signer.sendTransaction(await calculatorLibrary.populateTransaction.calcMint(requiredLpTokens, 0)) log(`Calculated ${daiCalculated} DAI required to mint 100,000 3Crv lp tokens`) expect(await daiToken.balanceOf(staker1.address), "DAI bal >= deposit").to.gte(daiCalculated) @@ -365,8 +361,8 @@ describe("Curve 3Pool calculations", async () => { const usdcBefore = await usdcToken.balanceOf(staker1.address) const lpBefore = await threeCrvToken.balanceOf(staker1.address) - const [usdcCalculated] = await poolCalculator.calcMint(requiredLpTokens, 1) - await staker1.signer.sendTransaction(await poolCalculator.populateTransaction.calcMint(requiredLpTokens, 1)) + const [usdcCalculated] = await calculatorLibrary.calcMint(requiredLpTokens, 1) + await staker1.signer.sendTransaction(await calculatorLibrary.populateTransaction.calcMint(requiredLpTokens, 1)) log(`Calculated ${usdcCalculated} USDC required to mint 100,000 3Crv lp tokens`) await usdcToken.connect(staker1.signer).approve(threePool.address, usdcCalculated) @@ -388,8 +384,8 @@ describe("Curve 3Pool calculations", async () => { const daiBefore = await daiToken.balanceOf(threeCrvWhale1.address) const lpBefore = await threeCrvToken.balanceOf(threeCrvWhale1.address) - const [daiCalculated] = await poolCalculator.calcRedeem(lpTokens, 0) - await staker1.signer.sendTransaction(await poolCalculator.populateTransaction.calcRedeem(lpTokens, 0)) + const [daiCalculated] = await calculatorLibrary.calcRedeem(lpTokens, 0) + await staker1.signer.sendTransaction(await calculatorLibrary.populateTransaction.calcRedeem(lpTokens, 0)) await threePool.connect(threeCrvWhale1.signer).remove_liquidity_one_coin(lpTokens, 0, 0) @@ -405,8 +401,8 @@ describe("Curve 3Pool calculations", async () => { const usdcBefore = await usdcToken.balanceOf(threeCrvWhale1.address) const lpBefore = await threeCrvToken.balanceOf(threeCrvWhale1.address) - const [usdcCalculated] = await poolCalculator.calcRedeem(lpTokens, 1) - await staker1.signer.sendTransaction(await poolCalculator.populateTransaction.calcRedeem(lpTokens, 1)) + const [usdcCalculated] = await calculatorLibrary.calcRedeem(lpTokens, 1) + await staker1.signer.sendTransaction(await calculatorLibrary.populateTransaction.calcRedeem(lpTokens, 1)) await threePool.connect(threeCrvWhale1.signer).remove_liquidity_one_coin(lpTokens, 1, 0) @@ -422,8 +418,8 @@ describe("Curve 3Pool calculations", async () => { const daiBefore = await daiToken.balanceOf(threeCrvWhale1.address) const lpBefore = await threeCrvToken.balanceOf(threeCrvWhale1.address) - const [lpCalculated] = await poolCalculator.calcWithdraw(daiTokens, 0) - await staker1.signer.sendTransaction(await poolCalculator.populateTransaction.calcWithdraw(daiTokens, 0)) + const [lpCalculated] = await calculatorLibrary.calcWithdraw(daiTokens, 0) + await staker1.signer.sendTransaction(await calculatorLibrary.populateTransaction.calcWithdraw(daiTokens, 0)) await threePool.connect(threeCrvWhale1.signer).remove_liquidity_imbalance([daiTokens, 0, 0], ethers.constants.MaxUint256) @@ -446,9 +442,9 @@ describe("Curve 3Pool calculations", async () => { const usdtAmount = simpleToExactAmount(308, USDT.decimals) const expectedThreeCrvTokens = BN.from("301500744564571495002") - const [calculated3Crv] = await poolCalculator.calcDeposit(usdtAmount, 2) + const [calculated3Crv] = await calculatorLibrary.calcDeposit(usdtAmount, 2) await account.signer.sendTransaction( - await poolCalculator.connect(account.signer).populateTransaction.calcDeposit(usdtAmount, 2), + await calculatorLibrary.connect(account.signer).populateTransaction.calcDeposit(usdtAmount, 2), ) expect(await usdtToken.balanceOf(account.address), "enough USDT to deposit").to.gte(usdtAmount) diff --git a/test-fork/vault/Convex3CrvBasicVault.spec.ts b/test-fork/vault/Convex3CrvBasicVault.spec.ts index e90afa0..e00a0ca 100644 --- a/test-fork/vault/Convex3CrvBasicVault.spec.ts +++ b/test-fork/vault/Convex3CrvBasicVault.spec.ts @@ -2,12 +2,12 @@ import { config } from "@tasks/deployment/convex3CrvVaults-config" import { resolveAddress } from "@tasks/utils/networkAddressFactory" import { musd3CRV, ThreeCRV } from "@tasks/utils/tokens" import shouldBehaveLikeBaseVault, { testAmounts } from "@test/shared/BaseVault.behaviour" -import { SAFE_INFINITY } from "@utils/constants" +import { ZERO, ZERO_ADDRESS } from "@utils/constants" import { impersonate, impersonateAccount } from "@utils/fork" import { StandardAccounts } from "@utils/machines" import { simpleToExactAmount } from "@utils/math" import { expect } from "chai" -import { Wallet } from "ethers" +import { ethers, Wallet } from "ethers" import * as hre from "hardhat" import { Convex3CrvBasicVault__factory, @@ -18,6 +18,7 @@ import { ICurve3Pool__factory, ICurveMetapool__factory, IERC20Metadata__factory, + MockERC20__factory, } from "types/generated" import { behaveLikeConvex3CrvVault, snapVault } from "./shared/Convex3Crv.behaviour" @@ -36,6 +37,7 @@ import type { ICurve3Pool, ICurveMetapool, IERC20Metadata, + MockERC20, } from "types/generated" import type { Convex3CrvContext } from "./shared/Convex3Crv.behaviour" @@ -126,7 +128,7 @@ describe("Convex 3Crv Basic Vault", async () => { metapool = ICurveMetapool__factory.connect(await vault.metapool(), owner.signer) baseRewardsPool = IConvexRewardsPool__factory.connect(await vault.baseRewardPool(), owner.signer) - await threeCrvToken.connect(owner.signer).approve(vault.address, SAFE_INFINITY) + await threeCrvToken.connect(owner.signer).approve(vault.address, ethers.constants.MaxUint256) const sa = new StandardAccounts() sa.default = owner @@ -227,7 +229,7 @@ describe("Convex 3Crv Basic Vault", async () => { metapool = ICurveMetapool__factory.connect(await vault.metapool(), owner.signer) baseRewardsPool = IConvexRewardsPool__factory.connect(await vault.baseRewardPool(), owner.signer) - await threeCrvToken.connect(owner.signer).approve(vault.address, SAFE_INFINITY) + await threeCrvToken.connect(owner.signer).approve(vault.address, ethers.constants.MaxUint256) await vault.connect(owner.signer)["deposit(uint256,address)"](initialDeposit, owner.address) ctx = { @@ -238,6 +240,7 @@ describe("Convex 3Crv Basic Vault", async () => { metapool, baseRewardsPool, dataEmitter, + convex3CrvCalculatorLibrary: factoryMetapoolCalculatorLibrary, amounts: { initialDeposit, deposit: initialDeposit.div(4), @@ -282,7 +285,7 @@ describe("Convex 3Crv Basic Vault", async () => { metapool = ICurveMetapool__factory.connect(await vault.metapool(), owner.signer) baseRewardsPool = IConvexRewardsPool__factory.connect(await vault.baseRewardPool(), owner.signer) - await threeCrvToken.connect(owner.signer).approve(vault.address, SAFE_INFINITY) + await threeCrvToken.connect(owner.signer).approve(vault.address, ethers.constants.MaxUint256) await vault.connect(owner.signer)["deposit(uint256,address)"](initialDeposit, owner.address) ctx = { vault: vault.connect(owner.signer), @@ -292,6 +295,7 @@ describe("Convex 3Crv Basic Vault", async () => { metapool, baseRewardsPool, dataEmitter, + convex3CrvCalculatorLibrary: factoryMetapoolCalculatorLibrary, amounts: { initialDeposit, deposit: initialDeposit.div(4), @@ -320,7 +324,7 @@ describe("Convex 3Crv Basic Vault", async () => { metapool = ICurveMetapool__factory.connect(await vault.metapool(), owner.signer) baseRewardsPool = IConvexRewardsPool__factory.connect(await vault.baseRewardPool(), owner.signer) - await threeCrvToken.connect(owner.signer).approve(vault.address, SAFE_INFINITY) + await threeCrvToken.connect(owner.signer).approve(vault.address, ethers.constants.MaxUint256) await vault.connect(owner.signer)["deposit(uint256,address)"](initialDeposit, owner.address) ctx = { vault: vault.connect(owner.signer), @@ -330,6 +334,7 @@ describe("Convex 3Crv Basic Vault", async () => { metapool, baseRewardsPool, dataEmitter, + convex3CrvCalculatorLibrary: factoryMetapoolCalculatorLibrary, amounts: { initialDeposit, deposit: initialDeposit.div(4), @@ -358,7 +363,7 @@ describe("Convex 3Crv Basic Vault", async () => { metapool = ICurveMetapool__factory.connect(await vault.metapool(), owner.signer) baseRewardsPool = IConvexRewardsPool__factory.connect(await vault.baseRewardPool(), owner.signer) - await threeCrvToken.connect(owner.signer).approve(vault.address, SAFE_INFINITY) + await threeCrvToken.connect(owner.signer).approve(vault.address, ethers.constants.MaxUint256) await vault.connect(owner.signer)["deposit(uint256,address)"](initialDeposit, owner.address) ctx = { vault: vault.connect(owner.signer), @@ -368,6 +373,7 @@ describe("Convex 3Crv Basic Vault", async () => { metapool, baseRewardsPool, dataEmitter, + convex3CrvCalculatorLibrary: factoryMetapoolCalculatorLibrary, amounts: { initialDeposit, deposit: initialDeposit.div(4), @@ -396,7 +402,7 @@ describe("Convex 3Crv Basic Vault", async () => { metapool = ICurveMetapool__factory.connect(await vault.metapool(), owner.signer) baseRewardsPool = IConvexRewardsPool__factory.connect(await vault.baseRewardPool(), owner.signer) - await threeCrvToken.connect(owner.signer).approve(vault.address, SAFE_INFINITY) + await threeCrvToken.connect(owner.signer).approve(vault.address, ethers.constants.MaxUint256) await vault.connect(owner.signer)["deposit(uint256,address)"](initialDeposit, owner.address) ctx = { vault: vault.connect(owner.signer), @@ -406,6 +412,7 @@ describe("Convex 3Crv Basic Vault", async () => { metapool, baseRewardsPool, dataEmitter, + convex3CrvCalculatorLibrary: factoryMetapoolCalculatorLibrary, amounts: { initialDeposit, deposit: initialDeposit.div(4), @@ -417,4 +424,38 @@ describe("Convex 3Crv Basic Vault", async () => { }) behaveLikeConvex3CrvVault(() => ctx) }) + describe("Curve3CrvFactoryMetapoolCalculatorLibrary", () => { + let emptyPool: MockERC20 + before("before", async () => { + await setup(normalBlock) + + emptyPool = await new MockERC20__factory(deployer).deploy("ERC20 Mock", "ERC20", 18, deployerAddress, 0) + }) + it("fails to calculate deposit in an empty pool", async () => { + await expect(factoryMetapoolCalculatorLibrary.calcDeposit(ZERO_ADDRESS, emptyPool.address, 500, 0)).to.be.revertedWith( + "empty pool", + ) + }) + it("fails to calculate mint in an empty pool", async () => { + await expect(factoryMetapoolCalculatorLibrary.calcMint(ZERO_ADDRESS, emptyPool.address, 500, 0)).to.be.revertedWith( + "empty pool", + ) + }) + it("fails to calculate withdraw in an empty pool", async () => { + await expect(factoryMetapoolCalculatorLibrary.calcWithdraw(ZERO_ADDRESS, emptyPool.address, 500, 0)).to.be.revertedWith( + "empty pool", + ) + }) + it("fails to calculate redeem in an empty pool", async () => { + await expect(factoryMetapoolCalculatorLibrary.calcRedeem(ZERO_ADDRESS, emptyPool.address, 500, 0)).to.be.revertedWith( + "empty pool", + ) + }) + it("converts with ZERO amounts", async () => { + expect(await factoryMetapoolCalculatorLibrary.convertUsdToBaseLp(ZERO)).to.be.eq(ZERO) + expect(await factoryMetapoolCalculatorLibrary.convertUsdToMetaLp(ZERO_ADDRESS, ZERO)).to.be.eq(ZERO) + expect(await factoryMetapoolCalculatorLibrary.convertToBaseLp(ZERO_ADDRESS, ZERO_ADDRESS, ZERO)).to.be.eq(ZERO) + expect(await factoryMetapoolCalculatorLibrary.convertToMetaLp(ZERO_ADDRESS, ZERO_ADDRESS, ZERO)).to.be.eq(ZERO) + }) + }) }) diff --git a/test-fork/vault/Convex3CrvLiquidatorVault.spec.ts b/test-fork/vault/Convex3CrvLiquidatorVault.spec.ts index 12dace4..4da6fa8 100644 --- a/test-fork/vault/Convex3CrvLiquidatorVault.spec.ts +++ b/test-fork/vault/Convex3CrvLiquidatorVault.spec.ts @@ -1,12 +1,12 @@ import { config } from "@tasks/deployment/convex3CrvVaults-config" import { resolveAddress } from "@tasks/utils/networkAddressFactory" import { CRV, CVX, DAI, ThreeCRV, USDC, USDT } from "@tasks/utils/tokens" -import { ONE_DAY, ONE_WEEK, SAFE_INFINITY } from "@utils/constants" +import { ONE_DAY, ONE_WEEK, SAFE_INFINITY, ZERO, ZERO_ADDRESS } from "@utils/constants" import { impersonateAccount, loadOrExecFixture } from "@utils/fork" -import { simpleToExactAmount } from "@utils/math" +import { BN, simpleToExactAmount } from "@utils/math" import { increaseTime } from "@utils/time" import { expect } from "chai" -import { keccak256, toUtf8Bytes } from "ethers/lib/utils" +import { getAddress, keccak256, toUtf8Bytes } from "ethers/lib/utils" import * as hre from "hardhat" import { Convex3CrvLiquidatorVault__factory, @@ -16,18 +16,28 @@ import { ICurve3Pool__factory, ICurveMetapool__factory, IERC20__factory, + MockERC20__factory, Nexus__factory, } from "types/generated" import { behaveLikeConvex3CrvVault, snapVault } from "./shared/Convex3Crv.behaviour" import type { Account } from "types/common" -import type { Convex3CrvLiquidatorVault, DataEmitter, IConvexRewardsPool, ICurve3Pool, ICurveMetapool, IERC20 } from "types/generated" +import type { + Convex3CrvLiquidatorVault, + Curve3CrvMetapoolCalculatorLibrary, + DataEmitter, + IConvexRewardsPool, + ICurve3Pool, + ICurveMetapool, + IERC20, + MockERC20, +} from "types/generated" import type { Convex3CrvContext } from "./shared/Convex3Crv.behaviour" -const keeperAddress = resolveAddress("OperationsSigner") const governorAddress = resolveAddress("Governor") +const keeperAddress = resolveAddress("OperationsSigner") const nexusAddress = resolveAddress("Nexus") const feeReceiver = resolveAddress("mStableDAO") const baseRewardPoolAddress = resolveAddress("CRVRewardsPool") @@ -50,7 +60,10 @@ describe("Convex 3Crv Liquidator Vault", async () => { let crvToken: IERC20 let cvxToken: IERC20 let daiToken: IERC20 + let usdcToken: IERC20 + let usdtToken: IERC20 let musdConvexVault: Convex3CrvLiquidatorVault + let calculatorLibrary: Curve3CrvMetapoolCalculatorLibrary let threePool: ICurve3Pool let metaPool: ICurveMetapool let baseRewardsPool: IConvexRewardsPool @@ -91,6 +104,8 @@ describe("Convex 3Crv Liquidator Vault", async () => { crvToken = IERC20__factory.connect(CRV.address, staker1.signer) cvxToken = IERC20__factory.connect(CVX.address, staker1.signer) daiToken = IERC20__factory.connect(DAI.address, mockLiquidator.signer) + usdcToken = IERC20__factory.connect(USDC.address, mockLiquidator.signer) + usdtToken = IERC20__factory.connect(USDT.address, mockLiquidator.signer) threePool = ICurve3Pool__factory.connect(curveThreePoolAddress, staker1.signer) metaPool = ICurveMetapool__factory.connect(curveMUSDPoolAddress, staker1.signer) baseRewardsPool = IConvexRewardsPool__factory.connect(baseRewardPoolAddress, staker1.signer) @@ -106,7 +121,7 @@ describe("Convex 3Crv Liquidator Vault", async () => { } const deployVault = async () => { - const calculatorLibrary = await new Curve3CrvMetapoolCalculatorLibrary__factory(keeper.signer).deploy() + calculatorLibrary = await new Curve3CrvMetapoolCalculatorLibrary__factory(keeper.signer).deploy() const libraryAddresses = { "contracts/peripheral/Curve/Curve3CrvMetapoolCalculatorLibrary.sol:Curve3CrvMetapoolCalculatorLibrary": calculatorLibrary.address, @@ -133,7 +148,7 @@ describe("Convex 3Crv Liquidator Vault", async () => { it("deploy and initialize Convex vault for mUSD pool", async () => { await setup(normalBlock) - const calculatorLibrary = await new Curve3CrvMetapoolCalculatorLibrary__factory(keeper.signer).deploy() + calculatorLibrary = await new Curve3CrvMetapoolCalculatorLibrary__factory(keeper.signer).deploy() const libraryAddresses = { "contracts/peripheral/Curve/Curve3CrvMetapoolCalculatorLibrary.sol:Curve3CrvMetapoolCalculatorLibrary": calculatorLibrary.address, @@ -217,6 +232,7 @@ describe("Convex 3Crv Liquidator Vault", async () => { metapool: metaPool, baseRewardsPool, dataEmitter, + convex3CrvCalculatorLibrary: calculatorLibrary, amounts: { initialDeposit, deposit: initialDeposit.div(4), @@ -249,7 +265,7 @@ describe("Convex 3Crv Liquidator Vault", async () => { }) }) describe("reward liquidations", () => { - before(async () => { + const liquidationSetup = async () => { await setup(normalBlock) // Deploy and initialize the vault @@ -260,15 +276,79 @@ describe("Convex 3Crv Liquidator Vault", async () => { await threeCrvToken.connect(staker2.signer).approve(musdConvexVault.address, mintAmount) await musdConvexVault.connect(staker2.signer).mint(mintAmount, staker2.address) + } + beforeEach(async () => { + await loadOrExecFixture(liquidationSetup) }) it("collect rewards for batch", async () => { await increaseTime(ONE_DAY) await musdConvexVault.collectRewards() }) it("donate DAI tokens back to vault", async () => { - const daiAmount = simpleToExactAmount(1000, DAI.decimals) - await daiToken.connect(mockLiquidator.signer).approve(musdConvexVault.address, daiAmount) - await musdConvexVault.connect(mockLiquidator.signer).donate(DAI.address, daiAmount) + const donateAmount = simpleToExactAmount(1000, DAI.decimals) + await daiToken.connect(mockLiquidator.signer).approve(musdConvexVault.address, donateAmount) + const tx = await musdConvexVault.connect(mockLiquidator.signer).donate(DAI.address, donateAmount) + + // Deposit event for the fee + await expect(tx) + .to.emit(musdConvexVault, "Deposit") + .withArgs(getAddress(mockLiquidator.address), feeReceiver, BN.from("9794853641497136119"), BN.from("9833364392187925988")) + + // Deposit event for the streaming of shares + await expect(tx) + .to.emit(musdConvexVault, "Deposit") + .withArgs( + getAddress(mockLiquidator.address), + musdConvexVault.address, + BN.from("969690510508216475829"), + BN.from("973503074826604672871"), + ) + }) + it("donate USDC tokens back to vault", async () => { + const donateAmount = simpleToExactAmount(2000, USDC.decimals) + await usdcToken.connect(mockLiquidator.signer).approve(musdConvexVault.address, donateAmount) + const tx = await musdConvexVault.connect(mockLiquidator.signer).donate(USDC.address, donateAmount) + + // Deposit event for the fee + await expect(tx) + .to.emit(musdConvexVault, "Deposit") + .withArgs(getAddress(mockLiquidator.address), feeReceiver, BN.from("19589780279497850787"), BN.from("19666801263319303387")) + + // Deposit event for the streaming of shares + await expect(tx) + .to.emit(musdConvexVault, "Deposit") + .withArgs( + getAddress(mockLiquidator.address), + musdConvexVault.address, + BN.from("1939388247670287227957"), + BN.from("1947013325068611035339"), + ) + }) + it("donate USDT tokens back to vault", async () => { + const donateAmount = simpleToExactAmount(3000, USDT.decimals) + await usdtToken.connect(mockLiquidator.signer).approve(musdConvexVault.address, donateAmount) + const tx = await musdConvexVault.connect(mockLiquidator.signer).donate(USDT.address, donateAmount) + + // Deposit event for the fee + await expect(tx) + .to.emit(musdConvexVault, "Deposit") + .withArgs(getAddress(mockLiquidator.address), feeReceiver, BN.from("29387432952198505245"), BN.from("29502974082101049187")) + + // Deposit event for the streaming of shares + await expect(tx) + .to.emit(musdConvexVault, "Deposit") + .withArgs( + getAddress(mockLiquidator.address), + musdConvexVault.address, + BN.from("2909355862267652019290"), + BN.from("2920794434128003869611"), + ) + }) + it("should fail to donate CRV tokens back to vault", async () => { + const donateAmount = simpleToExactAmount(1000, CRV.decimals) + await crvToken.connect(mockLiquidator.signer).approve(musdConvexVault.address, donateAmount) + const tx = musdConvexVault.connect(mockLiquidator.signer).donate(CRV.address, donateAmount) + await expect(tx).to.rejectedWith("token not in 3Pool") }) }) describe("set donate token", () => { @@ -310,4 +390,48 @@ describe("Convex 3Crv Liquidator Vault", async () => { expect(await musdConvexVault.donateToken(USDT.address), "donate token after").to.eq(USDT.address) }) }) + describe("Curve3CrvMetapoolCalculatorLibrary", () => { + let emptyPool: MockERC20 + before("before", async () => { + await setup(normalBlock) + calculatorLibrary = await new Curve3CrvMetapoolCalculatorLibrary__factory(keeper.signer).deploy() + emptyPool = await new MockERC20__factory(keeper.signer).deploy("ERC20 Mock", "ERC20", 18, keeperAddress, 0) + }) + it("fails to calculate deposit in an empty pool", async () => { + expect(await emptyPool.totalSupply()).to.be.eq(ZERO) + await expect(calculatorLibrary.calcDeposit(ZERO_ADDRESS, emptyPool.address, 500, 0)).to.be.revertedWith("empty pool") + }) + it("fails to calculate mint in an empty pool", async () => { + expect(await emptyPool.totalSupply()).to.be.eq(ZERO) + await expect(calculatorLibrary.calcMint(ZERO_ADDRESS, emptyPool.address, 500, 0)).to.be.revertedWith("empty pool") + }) + it("fails to calculate withdraw in an empty pool", async () => { + expect(await emptyPool.totalSupply()).to.be.eq(ZERO) + await expect(calculatorLibrary.calcWithdraw(ZERO_ADDRESS, emptyPool.address, 500, 0)).to.be.revertedWith("empty pool") + }) + it("fails to calculate redeem in an empty pool", async () => { + expect(await emptyPool.totalSupply()).to.be.eq(ZERO) + await expect(calculatorLibrary.calcRedeem(ZERO_ADDRESS, emptyPool.address, 500, 0)).to.be.revertedWith("empty pool") + }) + it("converts with ZERO amounts", async () => { + expect(await calculatorLibrary.convertUsdToBaseLp(ZERO), "convertUsdToBaseLp").to.be.eq(ZERO) + expect(await calculatorLibrary.convertUsdToMetaLp(ZERO_ADDRESS, ZERO), "convertUsdToBaseLp").to.be.eq(ZERO) + expect( + await calculatorLibrary["convertToBaseLp(address,address,uint256)"](ZERO_ADDRESS, ZERO_ADDRESS, ZERO), + "convertToBaseLp", + ).to.be.eq(ZERO) + expect( + await calculatorLibrary["convertToBaseLp(address,address,uint256,bool)"](ZERO_ADDRESS, ZERO_ADDRESS, ZERO, true), + "convertToBaseLp", + ).to.be.eq(ZERO) + expect( + await calculatorLibrary["convertToMetaLp(address,address,uint256)"](ZERO_ADDRESS, ZERO_ADDRESS, ZERO), + "convertToMetaLp", + ).to.be.eq(ZERO) + expect( + await calculatorLibrary["convertToMetaLp(address,address,uint256,bool)"](ZERO_ADDRESS, ZERO_ADDRESS, ZERO, true), + "convertToMetaLp", + ).to.be.eq(ZERO) + }) + }) }) diff --git a/test-fork/vault/Curve3CrvBasicMetaVault.spec.ts b/test-fork/vault/Curve3CrvBasicMetaVault.spec.ts index 790a5ae..5cda1aa 100644 --- a/test-fork/vault/Curve3CrvBasicMetaVault.spec.ts +++ b/test-fork/vault/Curve3CrvBasicMetaVault.spec.ts @@ -23,6 +23,7 @@ import type { Curve3CrvBasicMetaVault, ICurve3Pool, IERC20, IERC4626Vault } from import type { Curve3CrvContext } from "./shared/Curve3Crv.behaviour" const deployerAddress = resolveAddress("OperationsSigner") +const governorAddress = resolveAddress("Governor") const nexusAddress = resolveAddress("Nexus") const vaultManagerAddress = "0xeB2629a2734e272Bcc07BDA959863f316F4bD4Cf" const daiUserAddress = "0x075e72a5edf65f0a5f44699c7654c1a76941ddc8" // 250M at block 14810528 @@ -40,6 +41,7 @@ const slippageData = { describe("Curve 3Crv Basic Vault", async () => { let deployer: Signer + let governor: Account let threeCrvToken: IERC20 let threePool: ICurve3Pool let metaVault: IERC4626Vault @@ -60,6 +62,7 @@ describe("Curve 3Crv Basic Vault", async () => { }) } deployer = await impersonate(deployerAddress) + governor = await impersonateAccount(governorAddress) threeCrvToken = IERC20__factory.connect(ThreeCRV.address, deployer) threePool = ICurve3Pool__factory.connect(resolveAddress("CurveThreePool"), deployer) @@ -75,31 +78,6 @@ describe("Curve 3Crv Basic Vault", async () => { } } - it("initialize Curve 3Crv Meta Vault", async () => { - await commonSetup(normalBlock) - const vault = await deployContract( - new Curve3CrvBasicMetaVault__factory(curve3PoolCalculatorLibraryAddresses, deployer), - "Curve3CrvBasicMetaVault", - [nexusAddress, DAI.address, metaVault.address], - ) - await vault.initialize("3Pooler Meta Vault (DAI)", "3pDAI", vaultManagerAddress, slippageData) - - // Vault token data - expect(await vault.name(), "name").eq("3Pooler Meta Vault (DAI)") - expect(await vault.symbol(), "symbol").eq("3pDAI") - expect(await vault.decimals(), "decimals").eq(18) - - //Vault Slippages - expect(await vault.depositSlippage(), "depositSlippage").eq(slippageData.deposit) - expect(await vault.redeemSlippage(), "redeemSlippage").eq(slippageData.redeem) - expect(await vault.withdrawSlippage(), "withdrawSlippage").eq(slippageData.withdraw) - expect(await vault.mintSlippage(), "mintSlippage").eq(slippageData.mint) - - // Vault balances - expect(await vault.balanceOf(deployer.getAddress()), "balanceOf").eq(0) - expect(await vault.totalAssets(), "totalAssets").eq(0) - expect(await vault.totalSupply(), "totalSupply").eq(0) - }) const deployVault = async (asset: IERC20, owner: Account): Promise => { const vault = await new Curve3CrvBasicMetaVault__factory(curve3PoolCalculatorLibraryAddresses, deployer).deploy( nexusAddress, @@ -125,6 +103,84 @@ describe("Curve 3Crv Basic Vault", async () => { } } const ctx = {} + describe("initialize", () => { + before("before", async () => { + await commonSetup(normalBlock) + }) + it("curve 3Crv Meta Vault", async () => { + const vault = await deployContract( + new Curve3CrvBasicMetaVault__factory(curve3PoolCalculatorLibraryAddresses, deployer), + "Curve3CrvBasicMetaVault", + [nexusAddress, DAI.address, metaVault.address], + ) + await vault.initialize("3Pooler Meta Vault (DAI)", "3pDAI", vaultManagerAddress, slippageData) + + // Vault token data + expect(await vault.name(), "name").eq("3Pooler Meta Vault (DAI)") + expect(await vault.symbol(), "symbol").eq("3pDAI") + expect(await vault.decimals(), "decimals").eq(18) + + //Vault Slippages + expect(await vault.depositSlippage(), "depositSlippage").eq(slippageData.deposit) + expect(await vault.redeemSlippage(), "redeemSlippage").eq(slippageData.redeem) + expect(await vault.withdrawSlippage(), "withdrawSlippage").eq(slippageData.withdraw) + expect(await vault.mintSlippage(), "mintSlippage").eq(slippageData.mint) + + // Vault balances + expect(await vault.balanceOf(deployer.getAddress()), "balanceOf").eq(0) + expect(await vault.totalAssets(), "totalAssets").eq(0) + expect(await vault.totalSupply(), "totalSupply").eq(0) + }) + it("only initialize once", async () => { + const vault = await deployContract( + new Curve3CrvBasicMetaVault__factory(curve3PoolCalculatorLibraryAddresses, deployer), + "Curve3CrvBasicMetaVault", + [nexusAddress, DAI.address, metaVault.address], + ) + await vault.initialize("3Pooler Meta Vault (DAI)", "3pDAI", vaultManagerAddress, slippageData) + + await expect( + vault.initialize("3Pooler Meta Vault (DAI)", "3pDAI", vaultManagerAddress, slippageData), + "initialize twice", + ).to.be.revertedWith("Initializable: contract is already initialized") + }) + it("fails with wrong slippage data", async () => { + const vault = await deployContract( + new Curve3CrvBasicMetaVault__factory(curve3PoolCalculatorLibraryAddresses, deployer), + "Curve3CrvBasicMetaVault", + [nexusAddress, DAI.address, metaVault.address], + ) + const basisScale = await vault.BASIS_SCALE() + const wrongAmount = basisScale.add(1).toNumber() + const correctSlippageData = { + redeem: 101, + deposit: 99, + withdraw: 11, + mint: 10, + } + let slippageData = { ...correctSlippageData, deposit: wrongAmount } + await expect( + vault.initialize("3Pooler Meta Vault (DAI)", "3pDAI", vaultManagerAddress, slippageData), + "initialize twice", + ).to.be.revertedWith("Invalid deposit slippage") + slippageData = { ...correctSlippageData, mint: wrongAmount } + await expect( + vault.initialize("3Pooler Meta Vault (DAI)", "3pDAI", vaultManagerAddress, slippageData), + "initialize twice", + ).to.be.revertedWith("Invalid mint slippage") + slippageData = { ...correctSlippageData, withdraw: wrongAmount } + await expect( + vault.initialize("3Pooler Meta Vault (DAI)", "3pDAI", vaultManagerAddress, slippageData), + "initialize twice", + ).to.be.revertedWith("Invalid withdraw slippage") + slippageData = { ...correctSlippageData, redeem: wrongAmount } + await expect( + vault.initialize("3Pooler Meta Vault (DAI)", "3pDAI", vaultManagerAddress, slippageData), + "initialize twice", + ).to.be.revertedWith("Invalid redeem slippage") + }) + }) + describe("DAI 3Pooler Vault", () => { before(() => { // Anonymous functions cannot be used as fixtures so can't use arrow function @@ -134,6 +190,7 @@ describe("Curve 3Crv Basic Vault", async () => { // Reset ctx values from commonSetup ctx.threePool = threePool ctx.metaVault = metaVault + ctx.governor = governor // Asset specific values ctx.owner = await impersonateAccount(daiUserAddress) @@ -155,6 +212,7 @@ describe("Curve 3Crv Basic Vault", async () => { // Reset ctx values from commonSetup ctx.threePool = threePool ctx.metaVault = metaVault + ctx.governor = governor // Asset specific values ctx.owner = await impersonateAccount(usdcUserAddress) @@ -176,6 +234,7 @@ describe("Curve 3Crv Basic Vault", async () => { // Reset ctx values from commonSetup ctx.threePool = threePool ctx.metaVault = metaVault + ctx.governor = governor // Asset specific values ctx.owner = await impersonateAccount(usdtUserAddress) @@ -188,4 +247,19 @@ describe("Curve 3Crv Basic Vault", async () => { }) behaveLikeCurve3CrvVault(() => ctx) }) + describe("validations", () => { + before(async () => { + await commonSetup(normalBlock) + }) + + it("constructor should fail if asset is not in 3Pool", async () => { + const busdAddress = "0x4fabb145d64652a948d72533023f6e7a623c7c53" + const tx = new Curve3CrvBasicMetaVault__factory(curve3PoolCalculatorLibraryAddresses, deployer).deploy( + nexusAddress, + busdAddress, + metaVault.address, + ) + await expect(tx).to.be.revertedWith("Underlying asset not in 3Pool") + }) + }) }) diff --git a/test-fork/vault/savePlus.spec.ts b/test-fork/vault/savePlus.spec.ts index d01af06..9a16ce5 100644 --- a/test-fork/vault/savePlus.spec.ts +++ b/test-fork/vault/savePlus.spec.ts @@ -1100,15 +1100,9 @@ describe("Save+ Basic and Meta Vaults", async () => { ) // Expect underlying vaults with 0 balance until settlement - const vaultsDataAfter = await snapshotVaults( - convex3CrvLiquidatorVaults, - periodicAllocationPerfFeeMetaVault, - curve3CrvBasicMetaVaults, - threeCrvWhale1, - ) - expect(vaultsDataAfter.convex3CrvLiquidatorVaults.musd.totalSupply, "musd vault totalSupply").to.be.eq(0) - expect(vaultsDataAfter.convex3CrvLiquidatorVaults.frax.totalSupply, "frax vault totalSupply").to.be.eq(0) - expect(vaultsDataAfter.convex3CrvLiquidatorVaults.busd.totalSupply, "busd vault totalSupply").to.be.eq(0) + expect(await musdConvexVault.totalSupply(), "musd vault totalSupply").to.be.eq(0) + expect(await fraxConvexVault.totalSupply(), "frax vault totalSupply").to.be.eq(0) + expect(await busdConvexVault.totalSupply(), "busd vault totalSupply").to.be.eq(0) }) it("mint shares", async () => { await assertVaultMint( @@ -1118,16 +1112,11 @@ describe("Save+ Basic and Meta Vaults", async () => { dataEmitter, simpleToExactAmount(70000, ThreeCRV.decimals), ) + // Expect underlying vaults with 0 balance until settlement - const vaultsDataAfter = await snapshotVaults( - convex3CrvLiquidatorVaults, - periodicAllocationPerfFeeMetaVault, - curve3CrvBasicMetaVaults, - threeCrvWhale1, - ) - expect(vaultsDataAfter.convex3CrvLiquidatorVaults.musd.totalSupply, "musd vault totalSupply").to.be.eq(0) - expect(vaultsDataAfter.convex3CrvLiquidatorVaults.frax.totalSupply, "frax vault totalSupply").to.be.eq(0) - expect(vaultsDataAfter.convex3CrvLiquidatorVaults.busd.totalSupply, "busd vault totalSupply").to.be.eq(0) + expect(await musdConvexVault.totalSupply(), "musd vault totalSupply").to.be.eq(0) + expect(await fraxConvexVault.totalSupply(), "frax vault totalSupply").to.be.eq(0) + expect(await busdConvexVault.totalSupply(), "busd vault totalSupply").to.be.eq(0) }) it("settles to underlying vaults", async () => { const totalAssets = await periodicAllocationPerfFeeMetaVault.totalAssets() @@ -1188,6 +1177,23 @@ describe("Save+ Basic and Meta Vaults", async () => { expect(vaultsDataAfter.convex3CrvLiquidatorVaults.frax.feeReceiverBalance, "frax vault feeReceiverBalance").to.be.gt(0) expect(vaultsDataAfter.convex3CrvLiquidatorVaults.busd.feeReceiverBalance, "busd vault feeReceiverBalance").to.be.gt(0) }) + it("update assets per shares", async () => { + const assetsPerShareBefore = await periodicAllocationPerfFeeMetaVault.assetsPerShare() + const tx = await periodicAllocationPerfFeeMetaVault.connect(vaultManager.signer).updateAssetPerShare() + await expect(tx).to.emit(periodicAllocationPerfFeeMetaVault, "AssetsPerShareUpdated") + + const assetsPerShareAfter = await periodicAllocationPerfFeeMetaVault.assetsPerShare() + expect(assetsPerShareAfter).to.gt(assetsPerShareBefore) + }) + it("charge performance fee", async () => { + const perfAssetsPerShareBefore = await periodicAllocationPerfFeeMetaVault.perfFeesAssetPerShare() + + const tx = await periodicAllocationPerfFeeMetaVault.connect(vaultManager.signer).chargePerformanceFee() + + await expect(tx).not.to.emit(periodicAllocationPerfFeeMetaVault, "PerformanceFee") + const perfAssetsPerShareAfter = await periodicAllocationPerfFeeMetaVault.perfFeesAssetPerShare() + expect(perfAssetsPerShareAfter).to.lt(perfAssetsPerShareBefore) + }) }) describe("after settlement and burning vault shares", () => { it("partial withdraw", async () => { @@ -1226,6 +1232,52 @@ describe("Save+ Basic and Meta Vaults", async () => { ) }) }) + describe("after burning vault shares", () => { + it("deposit and settle while still burning", async () => { + const totalAssets = simpleToExactAmount(100000, ThreeCRV.decimals) + await assertVaultDeposit(threeCrvWhale1, threeCrvToken, periodicAllocationPerfFeeMetaVault, totalAssets) + + const musdSettlement = { vaultIndex: BN.from(0), assets: totalAssets.div(3) } + const fraxSettlement = { vaultIndex: BN.from(1), assets: totalAssets.div(3) } + const busdSettlement = { vaultIndex: BN.from(2), assets: totalAssets.div(3) } + const settlements = { musd: musdSettlement, frax: fraxSettlement, busd: busdSettlement } + await assertVaultSettle( + vaultManager, + convex3CrvLiquidatorVaults, + periodicAllocationPerfFeeMetaVault, + curve3CrvBasicMetaVaults, + settlements, + threeCrvWhale1, + ) + }) + it("deposit after stream has ended", async () => { + await increaseTime(ONE_WEEK) + + await assertVaultDeposit( + threeCrvWhale1, + threeCrvToken, + periodicAllocationPerfFeeMetaVault, + simpleToExactAmount(7000, ThreeCRV.decimals), + ) + }) + it("update assets per shares", async () => { + const assetsPerShareBefore = await periodicAllocationPerfFeeMetaVault.assetsPerShare() + const tx = await periodicAllocationPerfFeeMetaVault.connect(vaultManager.signer).updateAssetPerShare() + await expect(tx).to.emit(periodicAllocationPerfFeeMetaVault, "AssetsPerShareUpdated") + + const assetsPerShareAfter = await periodicAllocationPerfFeeMetaVault.assetsPerShare() + expect(assetsPerShareAfter).to.gt(assetsPerShareBefore) + }) + it("charge performance fee", async () => { + const perfAssetsPerShareBefore = await periodicAllocationPerfFeeMetaVault.perfFeesAssetPerShare() + + const tx = await periodicAllocationPerfFeeMetaVault.connect(vaultManager.signer).chargePerformanceFee() + + await expect(tx).to.emit(periodicAllocationPerfFeeMetaVault, "PerformanceFee") + const perfAssetsPerShareAfter = await periodicAllocationPerfFeeMetaVault.perfFeesAssetPerShare() + expect(perfAssetsPerShareAfter).to.gt(perfAssetsPerShareBefore) + }) + }) }) }) context("Curve3CrvBasicMetaVault", async () => { diff --git a/test-fork/vault/shared/Convex3Crv.behaviour.ts b/test-fork/vault/shared/Convex3Crv.behaviour.ts index eda283b..fb00423 100644 --- a/test-fork/vault/shared/Convex3Crv.behaviour.ts +++ b/test-fork/vault/shared/Convex3Crv.behaviour.ts @@ -14,6 +14,8 @@ import type { Account } from "types/common" import type { Convex3CrvBasicVault, Convex3CrvLiquidatorVault, + Curve3CrvFactoryMetapoolCalculatorLibrary, + Curve3CrvMetapoolCalculatorLibrary, DataEmitter, IConvexRewardsPool, ICurve3Pool, @@ -24,6 +26,7 @@ import type { const log = logger("test:Convex3CrvVault") export type Convex3CrvVault = Convex3CrvBasicVault | Convex3CrvLiquidatorVault +export type Convex3CrvCalculatorLibrary = Curve3CrvMetapoolCalculatorLibrary | Curve3CrvFactoryMetapoolCalculatorLibrary export interface VaultData { address: string @@ -117,6 +120,7 @@ export interface Convex3CrvContext { metapool: ICurveMetapool baseRewardsPool: IConvexRewardsPool dataEmitter: DataEmitter + convex3CrvCalculatorLibrary: Convex3CrvCalculatorLibrary amounts: { initialDeposit: BigNumber deposit: BigNumber @@ -323,6 +327,7 @@ export const behaveLikeConvex3CrvVault = (ctx: () => Convex3CrvContext): void => }) }) describe("EIP-4626 operations", () => { + let baseVirtualPriceBefore = BN.from(0) before(async () => { // Stop automine a new block with every transaction await ethers.provider.send("evm_setAutomine", [false]) @@ -331,6 +336,14 @@ export const behaveLikeConvex3CrvVault = (ctx: () => Convex3CrvContext): void => // Restore automine a new block with every transaction await ethers.provider.send("evm_setAutomine", [true]) }) + beforeEach(async () => { + baseVirtualPriceBefore = await ctx().convex3CrvCalculatorLibrary["getBaseVirtualPrice()"]() + }) + afterEach(async () => { + const baseVirtualPriceAfter = await ctx().convex3CrvCalculatorLibrary["getBaseVirtualPrice()"]() + expect(baseVirtualPriceBefore, "virtual price should not change").to.be.eq(baseVirtualPriceAfter) + }) + it("user deposits 3Crv assets to vault", async () => { const { amounts, threeCrvToken, owner, vault } = ctx() diff --git a/test-fork/vault/shared/Curve3Crv.behaviour.ts b/test-fork/vault/shared/Curve3Crv.behaviour.ts index e74d42c..df39f48 100644 --- a/test-fork/vault/shared/Curve3Crv.behaviour.ts +++ b/test-fork/vault/shared/Curve3Crv.behaviour.ts @@ -1,6 +1,6 @@ -import { logTxDetails } from "@tasks/utils" +import { logTxDetails, ThreeCRV } from "@tasks/utils" import { logger } from "@tasks/utils/logger" -import { ONE_DAY } from "@utils/constants" +import { ONE_DAY, ZERO } from "@utils/constants" import { loadOrExecFixture } from "@utils/fork" import { BN } from "@utils/math" import { increaseTime } from "@utils/time" @@ -10,7 +10,8 @@ import { ethers } from "hardhat" import type { BigNumber } from "ethers" import type { Account } from "types/common" -import type { Curve3CrvBasicMetaVault, ICurve3Pool, IERC20, IERC4626Vault } from "types/generated" +import { Curve3CrvBasicMetaVault, ICurve3Pool, IERC20, IERC20__factory, IERC4626Vault } from "types/generated" +import { assertBNClosePercent } from "@utils/assertions" const log = logger("test:Curve3CrvVault") @@ -29,6 +30,7 @@ export interface Curve3CrvContext { threePool: ICurve3Pool asset: IERC20 owner: Account + governor: Account fixture: () => Promise } export const behaveLikeCurve3CrvVault = (ctx: () => Curve3CrvContext): void => { @@ -71,6 +73,77 @@ export const behaveLikeCurve3CrvVault = (ctx: () => Curve3CrvContext): void => { } describe("EIP-4626", () => { + describe("view functions - fresh vault", () => { + before(async () => { + await loadOrExecFixture(ctx().fixture) + }) + it("totalAssets()", async () => { + const { metaVault, owner, vault } = ctx() + const totalMetaVaultShares = await metaVault.balanceOf(vault.address) + const total3CrvTokens = await metaVault.convertToAssets(totalMetaVaultShares) + const expectedAssets = await getAssetsFrom3CrvTokens(total3CrvTokens) + const actualTotalAssets = await vault.totalAssets() + expect(actualTotalAssets, "totalAssets").eq(expectedAssets) + log(`total assets ${actualTotalAssets} `) + log(`total shares ${await vault.totalSupply()} `) + }) + it("convertToAssets()", async () => { + const { metaVault, owner, vault, amounts } = ctx() + const metaVaultShares = await getMetaVaultSharesFromShares(amounts.mint) + const threeCrvTokens = await metaVault.convertToAssets(metaVaultShares) + const expectedAssets = await getAssetsFrom3CrvTokens(threeCrvTokens) + expect(await vault.convertToAssets(amounts.mint), "convertToAssets").eq(expectedAssets) + }) + it("convertToShares()", async () => { + const { metaVault, owner, vault, amounts } = ctx() + const threeCrvTokens = await get3CrvTokensFromAssets(amounts.deposit) + const metaVaultShares = await metaVault.convertToShares(threeCrvTokens) + const expectedShares = await getSharesFromMetaVaultShares(metaVaultShares) + expect(await vault.convertToShares(amounts.deposit), "convertToShares").eq(expectedShares) + }) + it("maxDeposit()", async () => { + const { vault, owner } = ctx() + expect(await vault.maxDeposit(owner.address), "maxDeposit").eq(ethers.constants.MaxUint256) + }) + it("maxMint()", async () => { + const { vault, owner } = ctx() + expect(await vault.maxMint(owner.address), "maxMint").eq(ethers.constants.MaxUint256) + }) + it("maxRedeem()", async () => { + const { vault, owner } = ctx() + expect(await vault.maxRedeem(owner.address), "maxMint").eq(await vault.balanceOf(owner.address)) + }) + it("maxWithdraw()", async () => { + const { vault, owner } = ctx() + const ownerShares = await vault.balanceOf(owner.address) + const expectedAssets = await vault.callStatic["redeem(uint256,address,address)"](ownerShares, owner.address, owner.address) + expect(await vault.maxWithdraw(owner.address), "maxWithdraw").eq(expectedAssets) + }) + it("user mints shares from vault", async () => { + const { amounts, metaVault, vault, owner } = ctx() + const receiver = owner.address + + const totalSharesBefore = await vault.totalSupply() + expect(totalSharesBefore, "total shares").to.be.eq(ZERO) + + const receiverSharesBefore = await vault.balanceOf(receiver) + + const tx = await vault.connect(owner.signer).mint(amounts.mint, receiver) + await logTxDetails(tx, "mint") + + const receivedShares = (await vault.balanceOf(receiver)).sub(receiverSharesBefore) + + expect(receivedShares, "Receiver received shares").eq(amounts.mint) + + expect(await vault.totalSupply(), "totalSupply").eq(totalSharesBefore.add(amounts.mint)) + + expect(await vault.totalAssets(), "totalAssets").eq( + await getAssetsFrom3CrvTokens( + await metaVault.convertToAssets(await getMetaVaultSharesFromShares(totalSharesBefore.add(amounts.mint))), + ), + ) + }) + }) describe("view functions", () => { before(async () => { await loadOrExecFixture(ctx().fixture) @@ -179,6 +252,12 @@ export const behaveLikeCurve3CrvVault = (ctx: () => Curve3CrvContext): void => { // Is only used to get gas usage using gasReporter await owner.signer.sendTransaction(await vault.populateTransaction.previewRedeem(amounts.redeem)) }) + it("redeem ZERO", async () => { + const { vault } = ctx() + const amount = ZERO + const staticPreviewAssets = await vault.previewRedeem(amount) + expect(staticPreviewAssets, "previewRedeem").to.eq(ZERO) + }) it("withdraw", async () => { const { amounts, vault, owner } = ctx() // Is only used to get gas usage using gasReporter @@ -189,6 +268,13 @@ export const behaveLikeCurve3CrvVault = (ctx: () => Curve3CrvContext): void => { const staticWithdrawShares = await vault.callStatic.withdraw(amounts.withdraw, owner.address, owner.address) expect(staticWithdrawShares, "previewWithdraw == static withdraw shares").to.eq(staticPreviewShares) }) + it("withdraw ZERO", async () => { + const { vault, owner } = ctx() + const amount = ZERO + + const staticPreviewShares = await vault.previewWithdraw(amount) + expect(staticPreviewShares, "previewWithdraw").to.eq(ZERO) + }) }) describe("vault operations", () => { before(async () => { @@ -350,6 +436,102 @@ export const behaveLikeCurve3CrvVault = (ctx: () => Curve3CrvContext): void => { ), ) }) + it("user redeems ZERO shares from vault", async () => { + const { metaVault, vault, owner, asset } = ctx() + const amount = ZERO + + await increaseTime(ONE_DAY) + const receiver = Wallet.createRandom().address + + const totalSharesBefore = await vault.totalSupply() + const totalAssetsBefore = await vault.totalAssets() + + const receiverAssetsBefore = await asset.balanceOf(receiver) + + const tx = await vault.connect(owner.signer)["redeem(uint256,address,address)"](amount, receiver, owner.address) + await logTxDetails(tx, "redeem") + await expect(tx).to.not.emit(vault, "Withdraw") + + const receivedAssets = (await asset.balanceOf(receiver)).sub(receiverAssetsBefore) + + expect(receivedAssets, "receiver received assets").eq(amount) + + expect(await vault.totalSupply(), "totalSupply").eq(totalSharesBefore) + expect(await vault.totalAssets(), "totalAssets").eq(totalAssetsBefore) + }) + it("user withdraws ZERO assets from vault", async () => { + const { metaVault, vault, owner, asset } = ctx() + const amount = ZERO + await increaseTime(ONE_DAY) + const receiver = Wallet.createRandom().address + + const totalSharesBefore = await vault.totalSupply() + const totalAssetsBefore = await vault.totalAssets() + + const receiverAssetsBefore = await asset.balanceOf(receiver) + + const tx = await vault.connect(owner.signer).withdraw(amount, receiver, owner.address) + await logTxDetails(tx, "withdraw") + await expect(tx).to.not.emit(vault, "Withdraw") + + const receivedAssets = (await asset.balanceOf(receiver)).sub(receiverAssetsBefore) + + expect(receivedAssets, "receiver received assets").eq(amount) + + expect(await vault.totalSupply(), "totalSupply").eq(totalSharesBefore) + expect(await vault.totalAssets(), "totalAssets").eq(totalAssetsBefore) + }) + }) + + describe("governor operations", () => { + before(async () => { + await loadOrExecFixture(ctx().fixture) + }) + it("governor resets allowance", async () => { + const { metaVault, vault, governor, asset, threePool } = ctx() + const lpToken = IERC20__factory.connect(ThreeCRV.address, governor.signer) + + await vault.connect(governor.signer).resetAllowances() + + expect(await asset.allowance(vault.address, threePool.address), "asset allowance").to.be.eq(ethers.constants.MaxUint256) + expect(await lpToken.allowance(vault.address, metaVault.address), "lpToken allowance").to.be.eq(ethers.constants.MaxUint256) + }) + + it("governor liquidates vault", async () => { + const { metaVault, vault, governor, owner, asset, amounts } = ctx() + // Deposits something into the vault + await vault.connect(owner.signer)["deposit(uint256,address)"](amounts.initialDeposit, owner.address) + + const totalMetaSharesBefore = await metaVault.balanceOf(vault.address) + const assetsGovBefore = await asset.balanceOf(governor.address) + + expect(totalMetaSharesBefore, "total shares").to.be.gt(ZERO) + const totalAssetsBefore = await vault.totalAssets() + const totalSupplyBefore = await vault.totalSupply() + + // Rescue tokens from a hack + await vault.connect(governor.signer).liquidateVault(ZERO) + + const totalMetaSharesAfter = await metaVault.balanceOf(vault.address) + const assetsGovAfter = await asset.balanceOf(governor.address) + const totalAssetsAfter = await vault.totalAssets() + const totalSupplyAfter = await vault.totalSupply() + + expect(totalAssetsAfter, "total assets").to.be.eq(ZERO) + expect(totalMetaSharesAfter, "total meta shares").to.be.eq(ZERO) + expect(totalSupplyAfter, "total supply does not change").to.be.eq(totalSupplyBefore) + expect(assetsGovAfter, "governor assets").to.be.gt(assetsGovBefore) + assertBNClosePercent(assetsGovAfter.sub(assetsGovBefore), totalAssetsBefore, "0.1", "assets rescued") + }) + + it("only governor resets allowance", async () => { + const { vault } = ctx() + await expect(vault.resetAllowances(), "resetAllowances").to.be.revertedWith("Only governor can execute") + }) + it("only governor liquidates vault", async () => { + const { vault } = ctx() + await expect(vault.liquidateVault(ZERO), "liquidateVault").to.be.revertedWith("Only governor can execute") + }) }) }) } diff --git a/test/shared/BaseVault.behaviour.ts b/test/shared/BaseVault.behaviour.ts index 11ca013..63ac27b 100644 --- a/test/shared/BaseVault.behaviour.ts +++ b/test/shared/BaseVault.behaviour.ts @@ -379,12 +379,12 @@ export function shouldBehaveLikeBaseVault(ctx: () => BaseVaultBehaviourContext): const { amounts } = ctx() await assertWithdraw(alice, bob, alice, amounts.withdraw) }) - it("from the vault sender != owner, infinite approval", async () => { + it("from the vault, sender != owner, infinite approval", async () => { const { amounts, vault } = ctx() await vault.connect(alice.signer).approve(bob.address, ethers.constants.MaxUint256) await assertWithdraw(bob, bob, alice, amounts.withdraw) }) - it("from the vault sender != owner, limited approval", async () => { + it("from the vault, sender != owner, limited approval", async () => { const { amounts, vault } = ctx() await vault.connect(alice.signer).approve(bob.address, 0) const shares = await vault.previewWithdraw(amounts.withdraw) @@ -474,12 +474,12 @@ export function shouldBehaveLikeBaseVault(ctx: () => BaseVaultBehaviourContext): it("from the vault, sender != receiver and sender = owner", async () => { await assertRedeem(alice, bob, alice, ctx().amounts.redeem) }) - it("from the vault sender != owner, infinite approval", async () => { + it("from the vault, sender != owner, infinite approval", async () => { const { amounts, vault } = ctx() await vault.connect(alice.signer).approve(bob.address, ethers.constants.MaxUint256) await assertRedeem(bob, bob, alice, amounts.redeem) }) - it("from the vault sender != owner, limited approval", async () => { + it("from the vault, sender != owner, limited approval", async () => { const { amounts, vault } = ctx() await vault.connect(alice.signer).approve(bob.address, 0) await vault.connect(alice.signer).approve(bob.address, amounts.redeem) @@ -515,7 +515,7 @@ export function shouldBehaveLikeBaseVault(ctx: () => BaseVaultBehaviourContext): const tx = vault.connect(sa.alice.signer).pause() await expect(tx).to.be.revertedWith("Only governor can execute") }) - it("pause successfull on governor call", async () => { + it("pause successful on governor call", async () => { const { vault, sa } = ctx() expect(await vault.paused()).to.not.equal(true) const tx = vault.connect(sa.governor.signer).pause() @@ -527,7 +527,7 @@ export function shouldBehaveLikeBaseVault(ctx: () => BaseVaultBehaviourContext): const tx = vault.connect(sa.alice.signer).unpause() await expect(tx).to.be.revertedWith("Only governor can execute") }) - it("unpause successfull on governor call", async () => { + it("unpause successful on governor call", async () => { const { vault, sa } = ctx() expect(await vault.paused()).to.not.equal(false) const tx = vault.connect(sa.governor.signer).unpause() diff --git a/test/vault/BaseVault.spec.ts b/test/vault/BaseVault.spec.ts index cef0ccf..3418099 100644 --- a/test/vault/BaseVault.spec.ts +++ b/test/vault/BaseVault.spec.ts @@ -71,6 +71,9 @@ const testVault = async (factory const tx = new factory(sa.default.signer).deploy(nexus.address, ZERO_ADDRESS) await expect(tx).to.be.revertedWith("Asset is zero") }) + it("should fail if nexus has zero address", async () => { + await expect(new factory(sa.default.signer).deploy(ZERO_ADDRESS, ZERO_ADDRESS)).to.be.revertedWith("Nexus address is zero") + }) }) describe("calling initialize", async () => { diff --git a/test/vault/allocate/PeriodicAllocationBasicVault.spec.ts b/test/vault/allocate/PeriodicAllocationBasicVault.spec.ts index 7b2bb6b..5238f8f 100644 --- a/test/vault/allocate/PeriodicAllocationBasicVault.spec.ts +++ b/test/vault/allocate/PeriodicAllocationBasicVault.spec.ts @@ -166,6 +166,11 @@ describe("PeriodicAllocationBasicVault", async () => { new PeriodicAllocationBasicVault__factory(sa.default.signer).deploy(nexus.address, ZERO_ADDRESS), ).to.be.revertedWith("Asset is zero") }) + it("should fail if nexus has zero address", async () => { + await expect( + new PeriodicAllocationBasicVault__factory(sa.default.signer).deploy(ZERO_ADDRESS, ZERO_ADDRESS), + ).to.be.revertedWith("Nexus address is zero") + }) }) describe("initialize", async () => { it("should properly store valid arguments", async () => { @@ -1143,7 +1148,28 @@ describe("PeriodicAllocationBasicVault", async () => { updatedAssetPerShare = initialDepositAmount.add(transferAmount).mul(assetsPerShareScale).div(initialDepositAmount) // validate post-rebalance properties and event - await expect(tx).to.emit(pabVault, "AssetsPerShareUpdated").withArgs(updatedAssetPerShare, initialDepositAmount.add(transferAmount)) + await expect(tx) + .to.emit(pabVault, "AssetsPerShareUpdated") + .withArgs(updatedAssetPerShare, initialDepositAmount.add(transferAmount)) + expect(await pabVault.assetsPerShare(), "assetPerShare").to.eq(updatedAssetPerShare) + }) + it("after removal of underlying vault", async () => { + // transfer some assets to bVault1 + await asset.transfer(bVault1.address, transferAmount) + + // validate pre-removal assetPerShare + expect(await pabVault.assetsPerShare(), "assetPerShare").to.eq(assetsPerShareScale) + + // Remove underlying vault + const tx = pabVault.connect(sa.governor.signer).removeVault(0) + + // calculate updatedAssetPerShare + updatedAssetPerShare = initialDepositAmount.add(transferAmount).mul(assetsPerShareScale).div(initialDepositAmount) + + // validate post-removal properties and event + await expect(tx) + .to.emit(pabVault, "AssetsPerShareUpdated") + .withArgs(updatedAssetPerShare, initialDepositAmount.add(transferAmount)) expect(await pabVault.assetsPerShare(), "assetPerShare").to.eq(updatedAssetPerShare) }) }) diff --git a/test/vault/allocate/SameAssetUnderlyingsBasicVault.spec.ts b/test/vault/allocate/SameAssetUnderlyingsBasicVault.spec.ts index c3c1c8d..8a6631a 100644 --- a/test/vault/allocate/SameAssetUnderlyingsBasicVault.spec.ts +++ b/test/vault/allocate/SameAssetUnderlyingsBasicVault.spec.ts @@ -83,6 +83,9 @@ describe("SameAssetUnderlyingsBasicVault", async () => { expect(await vaultDeployed.asset(), "asset").to.eq(asset.address) }) it("should fail if arguments are wrong", async () => { + await expect( + new SameAssetUnderlyingsBasicVault__factory(sa.default.signer).deploy(ZERO_ADDRESS, ZERO_ADDRESS), + ).to.be.revertedWith("Nexus address is zero") await expect( new SameAssetUnderlyingsBasicVault__factory(sa.default.signer).deploy(nexus.address, ZERO_ADDRESS), ).to.be.revertedWith("Asset is zero") diff --git a/test/vault/fees/PerfFeeBasicVault.spec.ts b/test/vault/fees/PerfFeeBasicVault.spec.ts index 77f035c..5a35d66 100644 --- a/test/vault/fees/PerfFeeBasicVault.spec.ts +++ b/test/vault/fees/PerfFeeBasicVault.spec.ts @@ -74,8 +74,12 @@ describe("Performance Fees", async () => { const tx = await vault.connect(sa.vaultManager.signer).chargePerformanceFee() - const assetsPerShareAfter = calculateAssetsPerShare(data) - const feeShares = calculateFeeShares(data, assetsPerShareAfter) + const assetsPerShareCurrent = calculateAssetsPerShare(data) + const feeShares = calculateFeeShares(data, assetsPerShareCurrent) + const assetsPerShareAfter = calculateAssetsPerShare({ + ...data, + totalShares: BN.from(data.totalShares).add(feeShares), + }) if (feeShares.gt(0)) { await expect(tx).to.emit(vault, "PerformanceFee").withArgs(feeReceiver.address, feeShares, assetsPerShareAfter) @@ -85,10 +89,8 @@ describe("Performance Fees", async () => { expect(await vault.balanceOf(feeReceiver.address), "fee shares after").to.eq(feeSharesBefore.add(feeShares)) const dataAfter = { - staker: data.staker, - stakerShares: data.stakerShares, + ...data, totalShares: feeShares.add(data.totalShares), - totalAssets: data.totalAssets, perfFeesAssetsPerShare: assetsPerShareAfter, } @@ -147,6 +149,11 @@ describe("Performance Fees", async () => { "Asset is zero", ) }) + it("should fail if nexus has zero address", async () => { + await expect(new PerfFeeBasicVault__factory(sa.default.signer).deploy(ZERO_ADDRESS, ZERO_ADDRESS)).to.be.revertedWith( + "Nexus address is zero", + ) + }) }) describe("behaviors", async () => { describe("should behave like AbstractVaultBehaviourContext", async () => { @@ -330,14 +337,19 @@ describe("Performance Fees", async () => { totalAssets: totalAssets, } const feeShares = calculateFeeShares(data, calculateAssetsPerShare(data)) + const assetsPerShareAfter = calculateAssetsPerShare({ + ...data, + totalShares: data.totalShares.add(feeShares), + }) const newPerfFee = 100 - expect(await vault.performanceFee(), "PerformaceFees").to.not.eq(newPerfFee) + expect(await vault.performanceFee(), "PerformanceFees").to.not.eq(newPerfFee) const tx = vault.connect(sa.governor.signer).setPerformanceFee(newPerfFee) - await expect(tx).to.emit(vault, "PerformanceFee").withArgs(feeReceiver.address, feeShares, calculateAssetsPerShare(data)) + await expect(tx).to.emit(vault, "PerformanceFee").withArgs(feeReceiver.address, feeShares, assetsPerShareAfter) expect(tx).to.emit(vault, "PerformanceFeeUpdated").withArgs(newPerfFee) - expect(await vault.performanceFee(), "PerformaceFees").to.eq(newPerfFee) + expect(tx).to.emit(vault, "AssetsPerShareUpdated") + expect(await vault.performanceFee(), "PerformanceFees").to.eq(newPerfFee) }) }) describe("set fee receiver", async () => { diff --git a/test/vault/liquidator/Liquidator.spec.ts b/test/vault/liquidator/Liquidator.spec.ts index 21e7ab1..f0798de 100644 --- a/test/vault/liquidator/Liquidator.spec.ts +++ b/test/vault/liquidator/Liquidator.spec.ts @@ -12,6 +12,10 @@ import { LiquidatorBasicVault__factory, MockERC20__factory, MockNexus__factory, + MockLiquidatorMaliciousVault__factory, + MockLiquidatorMaliciousVault, + MockMaliciousDexSwap__factory, + MockMaliciousDexSwap, } from "types/generated" import { buildDonateTokensInput } from "../../../tasks/utils/liquidatorUtil" @@ -41,6 +45,7 @@ const ERROR = { ALREADY_DONATED: "already donated", WRONG_INPUT: "Wrong input", DONATE_WRONG_TOKEN: "Donated token not asset", + REENTRY_GARD: "ReentrancyGuard: reentrant call", } describe("Liquidator", async () => { @@ -59,6 +64,9 @@ describe("Liquidator", async () => { // async conf let asyncSwapper: CowSwapDex let relayer: MockGPv2VaultRelayer + // malicious contracts + let vaultMalicious: MockLiquidatorMaliciousVault + let syncSwapperMalicious: MockMaliciousDexSwap let vault1Account: Signer let vault2Account: Signer @@ -114,6 +122,11 @@ describe("Liquidator", async () => { await asyncSwapper.connect(sa.governor.signer).approveToken(rewards2.address) await asyncSwapper.connect(sa.governor.signer).approveToken(rewards3.address) await relayer.initialize(exchanges) + + // deploy malicious + + syncSwapperMalicious = await new MockMaliciousDexSwap__factory(sa.default.signer).deploy(nexus.address) + await syncSwapperMalicious.initialize(exchanges) } const setup = async () => { nexus = await new MockNexus__factory(sa.default.signer).deploy(sa.governor.address) @@ -139,6 +152,9 @@ describe("Liquidator", async () => { vault3 = await new LiquidatorBasicVault__factory(sa.default.signer).deploy(nexus.address, asset1.address) await vault3.initialize("Vault 3", "V3", sa.default.address, [rewards1.address]) + vaultMalicious = await new MockLiquidatorMaliciousVault__factory(sa.default.signer).deploy(nexus.address, asset1.address) + await vaultMalicious.initialize("Vault R", "VR", sa.default.address, [rewards1.address]) + // to simulate calls from the vault vault1Account = await impersonate(vault1.address) vault2Account = await impersonate(vault2.address) @@ -388,10 +404,14 @@ describe("Liquidator", async () => { expect(event.args.purchaseTokens[2][0], "purchase token vault3 rewards 1").to.eq(asset1.address) }) describe("failed as", () => { - it("not keep or governor", async () => { + it("not keeper or governor", async () => { const tx = liquidator.connect(sa.default.signer).collectRewards([vault3.address]) await expect(tx).to.revertedWith(ERROR.ONLY_KEEPER_GOVERNOR) }) + it("reentrant call", async () => { + const tx = liquidator.collectRewards([vaultMalicious.address]) + await expect(tx).to.revertedWith(ERROR.REENTRY_GARD) + }) }) }) describe("sync swap rewards for assets", () => { @@ -653,6 +673,12 @@ describe("Liquidator", async () => { const tx = liquidator.connect(sa.keeper.signer).swap(asset1.address, asset2.address, asset1Amount, "0x") await expect(tx).to.revertedWith(ERROR.INVALID_SWAP) }) + it("reentrant call", async () => { + await liquidator.connect(sa.governor.signer).setSyncSwapper(syncSwapperMalicious.address) + const tx = liquidator.connect(sa.keeper.signer).swap(rewards1.address, asset1.address, 0, "0x") + await expect(tx).to.revertedWith(ERROR.REENTRY_GARD) + await liquidator.connect(sa.governor.signer).setSyncSwapper(syncSwapper.address) + }) it("no reward", async () => { // successfully swap so a new liquidation is created with no rewards await liquidator.connect(sa.keeper.signer).swap(rewards1.address, asset1.address, asset1Amount, "0x") diff --git a/test/vault/liquidator/LiquidatorStreamFeeVault.spec.ts b/test/vault/liquidator/LiquidatorStreamFeeVault.spec.ts index d9c29e4..4e2e097 100644 --- a/test/vault/liquidator/LiquidatorStreamFeeVault.spec.ts +++ b/test/vault/liquidator/LiquidatorStreamFeeVault.spec.ts @@ -247,6 +247,11 @@ describe("Streamed Liquidator Fee Vault", async () => { new LiquidatorStreamFeeBasicVault__factory(sa.default.signer).deploy(nexus.address, ZERO_ADDRESS, ONE_DAY), ).to.be.revertedWith("Asset is zero") }) + it("should fail if nexus has zero address", async () => { + await expect( + new LiquidatorStreamFeeBasicVault__factory(sa.default.signer).deploy(ZERO_ADDRESS, ZERO_ADDRESS, ONE_DAY), + ).to.be.revertedWith("Nexus address is zero") + }) }) describe("calling initialize", async () => { before(async () => { diff --git a/test/vault/liquidator/LiquidatorStreamVault.spec.ts b/test/vault/liquidator/LiquidatorStreamVault.spec.ts index c0e1d56..be3c90e 100644 --- a/test/vault/liquidator/LiquidatorStreamVault.spec.ts +++ b/test/vault/liquidator/LiquidatorStreamVault.spec.ts @@ -204,6 +204,11 @@ describe("Streamed Liquidator Vault", async () => { new LiquidatorStreamBasicVault__factory(sa.default.signer).deploy(nexus.address, ZERO_ADDRESS, ONE_DAY), ).to.be.revertedWith("Asset is zero") }) + it("should fail if nexus has zero address", async () => { + await expect( + new LiquidatorStreamBasicVault__factory(sa.default.signer).deploy(ZERO_ADDRESS, ZERO_ADDRESS, ONE_DAY), + ).to.be.revertedWith("Nexus address is zero") + }) }) describe("behaviors", async () => { shouldBehaveLikeVaultManagerRole(() => ({ vaultManagerRole: vault as VaultManagerRole, sa })) diff --git a/test/vault/liquidity/BasicSlippage.spec.ts b/test/vault/liquidity/BasicSlippage.spec.ts index 25490da..ceb16ab 100644 --- a/test/vault/liquidity/BasicSlippage.spec.ts +++ b/test/vault/liquidity/BasicSlippage.spec.ts @@ -32,7 +32,9 @@ describe("BasicSlippage", () => { shouldBehaveLikeVaultManagerRole(() => ({ vaultManagerRole: slippage as VaultManagerRole, sa })) }) it("fails if initialize is called more than once", async () => { - await expect(slippage.initialize(sa.vaultManager.address, initialSlippage)).to.be.revertedWith("Initializable: contract is already initialized") + await expect(slippage.initialize(sa.vaultManager.address, initialSlippage)).to.be.revertedWith( + "Initializable: contract is already initialized", + ) }) it("post deploy", async () => { await expect(initTx).to.emit(slippage, "MintSlippageChange").withArgs(sa.default.address, 99) @@ -70,7 +72,7 @@ describe("BasicSlippage", () => { }) it("should fail if invalid value", async () => { const tx = slippage.connect(sa.governor.signer).setDepositSlippage((await slippage.BASIS_SCALE()).add(1)) - await expect(tx).to.be.revertedWith("Invalid deposit Slippage") + await expect(tx).to.be.revertedWith("Invalid deposit slippage") }) it("should correctly update", async () => { expect(await slippage.depositSlippage(), "deposit").to.not.eq(89) @@ -86,7 +88,7 @@ describe("BasicSlippage", () => { }) it("should fail if invalid value", async () => { const tx = slippage.connect(sa.governor.signer).setWithdrawSlippage((await slippage.BASIS_SCALE()).add(1)) - await expect(tx).to.be.revertedWith("Invalid withdraw Slippage") + await expect(tx).to.be.revertedWith("Invalid withdraw slippage") }) it("should correctly update", async () => { expect(await slippage.withdrawSlippage(), "withdraw").to.not.eq(90) diff --git a/test/vault/liquidity/curve/Curve3CrvBasicMetaVault.spec.ts b/test/vault/liquidity/curve/Curve3CrvBasicMetaVault.spec.ts new file mode 100644 index 0000000..b023874 --- /dev/null +++ b/test/vault/liquidity/curve/Curve3CrvBasicMetaVault.spec.ts @@ -0,0 +1,62 @@ +import { DEAD_ADDRESS, ZERO_ADDRESS } from "@utils/constants" +import { ContractMocks, StandardAccounts } from "@utils/machines" +import { expect } from "chai" +import { ethers } from "hardhat" +import { Curve3CrvBasicMetaVault__factory, Curve3PoolCalculatorLibrary__factory } from "types/generated" +import type { MockERC20, MockNexus, Curve3CrvBasicMetaVault } from "types/generated" +import { Curve3CrvBasicMetaVaultLibraryAddresses } from "types/generated/factories/contracts/vault/liquidity/curve/Curve3CrvBasicMetaVault__factory" + +describe("Curve3CrvBasicMetaVault", () => { + /* -- Declare shared variables -- */ + let sa: StandardAccounts + let mocks: ContractMocks + let nexus: MockNexus + + // Testing contract + let vault: Curve3CrvBasicMetaVault + let asset: MockERC20 + let curve3PoolCalculatorLibraryAddresses: Curve3CrvBasicMetaVaultLibraryAddresses + + /* -- Declare shared functions -- */ + const setup = async () => { + const accounts = await ethers.getSigners() + sa = await new StandardAccounts().initAccounts(accounts) + + mocks = await new ContractMocks().init(sa) + nexus = mocks.nexus + asset = mocks.erc20 + + const threePoolCalculatorLibrary = await new Curve3PoolCalculatorLibrary__factory(sa.default.signer).deploy() + curve3PoolCalculatorLibraryAddresses = { + "contracts/peripheral/Curve/Curve3PoolCalculatorLibrary.sol:Curve3PoolCalculatorLibrary": threePoolCalculatorLibrary.address, + } + } + before("init contract", async () => { + await setup() + }) + + describe("constructor", async () => { + it("should fail if asset has zero address", async () => { + let tx = new Curve3CrvBasicMetaVault__factory(curve3PoolCalculatorLibraryAddresses, sa.default.signer).deploy( + ZERO_ADDRESS, + ZERO_ADDRESS, + ZERO_ADDRESS, + ) + await expect(tx).to.be.revertedWith("Nexus address is zero") + + tx = new Curve3CrvBasicMetaVault__factory(curve3PoolCalculatorLibraryAddresses, sa.default.signer).deploy( + nexus.address, + ZERO_ADDRESS, + ZERO_ADDRESS, + ) + await expect(tx).to.be.revertedWith("Asset is zero") + + tx = new Curve3CrvBasicMetaVault__factory(curve3PoolCalculatorLibraryAddresses, sa.default.signer).deploy( + nexus.address, + DEAD_ADDRESS, + ZERO_ADDRESS, + ) + await expect(tx).to.be.revertedWith("Invalid Vault") + }) + }) +}) diff --git a/web-config.ts b/web-config.ts index 95d337b..504b050 100644 --- a/web-config.ts +++ b/web-config.ts @@ -137,11 +137,12 @@ const bundle = () => { keywords, main: names.main, types: names.types, - publishConfig: { - registry: "https://npm.pkg.github.com/", - email: "info@mstable.com", - scope: "@mstable", - }, + // Enable if it is desired to publish to git hub packages + // publishConfig: { + // registry: "https://npm.pkg.github.com/", + // email: "info@mstable.com", + // scope: "@mstable", + // }, }) } catch (e) { console.error("Error writing package.json ", e) @@ -174,10 +175,10 @@ const publish = () => { } } -;(async () => { - clean() - compile() - bundle() - //publish() - //clean() -})() + ; (async () => { + clean() + compile() + bundle() + //publish() + //clean() + })() diff --git a/yarn.lock b/yarn.lock index b846e4c..b7fe87f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3978,7 +3978,7 @@ growl@1.10.5: resolved "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz" integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== -handlebars@^4.0.1: +handlebars@^4.0.1, handlebars@^4.7.7: version "4.7.7" resolved "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz" integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA== @@ -6416,6 +6416,11 @@ solhint@^3.3.7: optionalDependencies: prettier "^1.14.3" +solidity-ast@^0.4.31: + version "0.4.35" + resolved "https://registry.yarnpkg.com/solidity-ast/-/solidity-ast-0.4.35.tgz#82e064b14dc989338123264bde2235cad751f128" + integrity sha512-F5bTDLh3rmDxRmLSrs3qt3nvxJprWSEkS7h2KmuXDx7XTfJ6ZKVTV1rtPIYCqJAuPsU/qa8YUeFn7jdOAZcTPA== + solidity-comments-extractor@^0.0.7: version "0.0.7" resolved "https://registry.npmjs.org/solidity-comments-extractor/-/solidity-comments-extractor-0.0.7.tgz" @@ -6447,6 +6452,14 @@ solidity-coverage@0.8.2: shelljs "^0.8.3" web3-utils "^1.3.6" +solidity-docgen@^0.6.0-beta.29: + version "0.6.0-beta.29" + resolved "https://registry.yarnpkg.com/solidity-docgen/-/solidity-docgen-0.6.0-beta.29.tgz#90f0382091b56f103b185b2fbf869ff399b2d361" + integrity sha512-63p3w6wj1WFhhC8pXTI3bz5qUTFuGmLNHFnwwpjZ6Qv8dF2WGDt0pg1rbA6c3bL/A4d0ATN66Mte1saGKVWdHg== + dependencies: + handlebars "^4.7.7" + solidity-ast "^0.4.31" + source-map-support@^0.5.13: version "0.5.21" resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz"