diff --git a/.dapprc b/.dapprc index 44c6b44..df906bc 100644 --- a/.dapprc +++ b/.dapprc @@ -1,12 +1,16 @@ # Basic build/test configuration. -export DAPP_SOLC_VERSION=0.8.9 +export DAPP_SOLC_VERSION=0.8.10 export DAPP_BUILD_OPTIMIZE=1 -export DAPP_BUILD_OPTIMIZE_RUNS=1000000 +export DAPP_BUILD_OPTIMIZE_RUNS=8000 export DAPP_LINK_TEST_LIBRARIES=0 export DAPP_TEST_VERBOSITY=1 export DAPP_TEST_SMTTIMEOUT=500000 -if [ "$DEEP_FUZZ" == "true" ] +# These normally default to 0 which breaks some tests. +export DAPP_TEST_TIMESTAMP=1000000000 +export DAPP_TEST_NUMBER=10000000 + +if [ "$DEEP_FUZZ" = "true" ] then export DAPP_TEST_FUZZ_RUNS=10000 # Fuzz for a long time if DEEP_FUZZ is set to true. else diff --git a/.gas-snapshot b/.gas-snapshot index 2caa3a3..3a61a50 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,37 +1,72 @@ -testFailWithdrawFromStrategyWithNotEnoughBalance() (gas: 300443) -testAtomicDepositRedeem() (gas: 208401) -testAtomicEnterExitSinglePool() (gas: 387115) -testFailDepositIntoStrategyZero() (gas: 28111) -testFailWithdrawFromStrategyZero() (gas: 28156) -testAtomicDepositWithdraw() (gas: 208445) -testPushingToWithdrawalQueue() (gas: 129874) -testFailDepositIntoStrategyWithNoBalance() (gas: 130185) -testFailRedeemWithNotEnoughBalance() (gas: 153411) -testUnprofitableHarvest() (gas: 522343) -testFailDepositWithNotEnoughApproval() (gas: 121418) -testFailDepositIntoStrategyWithNotEnoughBalance() (gas: 274096) -testFailWithdrawFromStrategyWithNoBalance() (gas: 28413) -testFailRedeemZero() (gas: 1184) -testFailWithdrawWithNotEnoughBalance() (gas: 153401) -testReplaceWithdrawalQueueIndex() (gas: 124837) -testFailWithdrawWithIncompleteQueue() (gas: 521082) -testProfitableHarvest() (gas: 585931) -testFailDepositZero() (gas: 1208) -testFailWithdrawWithNoBalance() (gas: 3564) -testPoppingFromWithdrawalQueue() (gas: 143346) -testFailWithdrawZero() (gas: 1163) -testFailWithdrawFromStrategyWithoutTrust() (gas: 302863) -testFailDepositWithNoApproval() (gas: 51113) -testFailRedeemWithNoBalance() (gas: 3638) -testAtomicEnterExitMultiPool() (gas: 531020) -testReplaceWithdrawalQueueIndexWithTip() (gas: 126949) -testFailTrustStrategyWithWrongUnderlying() (gas: 2002337) -testSwapWithdrawalQueueIndexes() (gas: 127230) -testFailWithdrawWithEmptyQueue() (gas: 313058) -testFailDepositIntoNotWETHVault() (gas: 10760) -testFailRedeemFromNotWETHVault() (gas: 245181) -testAtomicDepositRedeemETH() (gas: 329700) -testFailWithdrawFromNotWETHVault() (gas: 245115) -testAtomicDepositWithdrawETH() (gas: 331361) -testFailNoDuplicateVaults() (gas: 272747603310794) -testDeployVault() (gas: 4749517) +testIntegration() (gas: 5105473) +testFailDeposit() (gas: 80992) +testInitializeAndDeposit() (gas: 173385) +testTrustStrategyWithWETHUnderlying() (gas: 28084) +testTrustStrategyWithETHUnderlying() (gas: 27502) +testAtomicDepositWithdrawIntoETHStrategies() (gas: 618638) +testDestroyVaultReturnsETH() (gas: 13378) +testFailWithdrawFromStrategyWithNotEnoughBalance() (gas: 298484) +testAtomicDepositRedeem() (gas: 203256) +testFailPushStackFull() (gas: 118311) +testUpdatingHarvestDelay() (gas: 78825) +testFailPopStackEmpty() (gas: 3568) +testAtomicEnterExitSinglePool() (gas: 378899) +testFailHarvestUntrustedStrategy() (gas: 324767) +testAtomicDepositWithdraw() (gas: 203300) +testFailSetStackTooBig() (gas: 5493) +testDestroyVault() (gas: 6316) +testFailDepositIntoStrategyWithNoBalance() (gas: 129069) +testWithdrawingWithUntrustedStrategyInStack() (gas: 1759083) +testFailRedeemWithNotEnoughBalance() (gas: 152131) +testUnprofitableHarvest() (gas: 541816) +testPoppingFromWithdrawalStack() (gas: 147691) +testPushingToWithdrawalStack() (gas: 132102) +testFailDepositWithNotEnoughApproval() (gas: 120370) +testFailDepositIntoStrategyWithNotEnoughBalance() (gas: 271975) +testSeizeStrategyWithBalanceGreaterThanTotalAssets() (gas: 482498) +testWithdrawingWithDuplicateStrategiesInStack() (gas: 667651) +testFailWithdrawFromStrategyWithNoBalance() (gas: 28736) +testFailWithdrawWithEmptyStack() (gas: 310196) +testClaimFees() (gas: 200865) +testFailWithdrawWithNotEnoughBalance() (gas: 152145) +testSeizeStrategy() (gas: 436037) +testFailTrustStrategyWithETHUnderlying() (gas: 933256) +testProfitableHarvest() (gas: 577935) +testFailWithdrawWithIncompleteStack() (gas: 517606) +testFailWithdrawWithNoBalance() (gas: 3628) +testReplaceWithdrawalStackIndexWithTip() (gas: 128330) +testReplaceWithdrawalStackIndex() (gas: 126510) +testFailHarvestAfterWindowBeforeDelay() (gas: 471999) +testFailInitializeTwice() (gas: 1542) +testMultipleHarvestsInWindow() (gas: 525917) +testFailWithdrawFromStrategyWithoutTrust() (gas: 301422) +testFailDepositWithNoApproval() (gas: 50047) +testFailRedeemWithNoBalance() (gas: 3635) +testAtomicEnterExitMultiPool() (gas: 520005) +testFailTrustStrategyWithWrongUnderlying() (gas: 1889496) +testSwapWithdrawalStackIndexes() (gas: 128780) +testSetDefaultTargetFloatPercent() (gas: 55620) +testDefaultHarvestDelayOverridesCustomOfZero() (gas: 58529) +testDefaultTargetFloatPercentOverridesCustomOfZero() (gas: 57765) +testCustomHarvestDelayOverridesDefault() (gas: 78322) +testSetCustomHarvestDelay() (gas: 55230) +testCustomHarvestWindowOverridesDefault() (gas: 88769) +testSetCustomFeePercent() (gas: 54616) +testSetDefaultFeePercent() (gas: 55683) +testSetCustomHarvestWindow() (gas: 86131) +testCustomTargetFloatPercentOverridesDefault() (gas: 77607) +testDefaultFeePercentOverridesCustomOfZero() (gas: 57860) +testSetDefaultHarvestWindow() (gas: 66772) +testDefaultHarvestWindowOverridesCustomOfZero() (gas: 68987) +testSetCustomTargetFloatPercent() (gas: 54473) +testSetDefaultHarvestDelay() (gas: 56390) +testIsVaultDeployed() (gas: 3690) +testFailNoDuplicateVaults() (gas: 272747603290173) +testDeployVault() (gas: 4074195) +testVaultCreation() (gas: 4232391) +testSetConfigurationModule() (gas: 3498) +testFailDepositIntoNotWETHVault() (gas: 10656) +testFailRedeemFromNotWETHVault() (gas: 243420) +testAtomicDepositRedeemETH() (gas: 325421) +testFailWithdrawFromNotWETHVault() (gas: 243320) +testAtomicDepositWithdrawETH() (gas: 326998) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index feebcd7..94dae4d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,6 +17,9 @@ jobs: - name: Install dependencies run: nix-shell --run 'make' + - name: Check gas snapshots + run: nix-shell --run 'dapp check-snapshot' + - name: Run tests run: nix-shell --run 'dapp test' env: diff --git a/.gitignore b/.gitignore index 88cb115..ba09aa9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /out +/cache /node_modules \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index 9a900b7..0018237 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "lib/solmate"] path = lib/solmate url = https://github.com/rari-capital/solmate +[submodule "lib/forge-std"] + path = lib/forge-std + url = https://github.com/brockelmore/forge-std diff --git a/.vscode/settings.json b/.vscode/settings.json index 044e3ff..b399aa6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,9 +1,10 @@ { "solidity.packageDefaultDependenciesContractsDirectory": "src", "solidity.packageDefaultDependenciesDirectory": "lib", - "solidity.compileUsingRemoteVersion": "v0.8.9", + "solidity.compileUsingRemoteVersion": "v0.8.10+commit.fc410830", "search.exclude": { "lib": true }, "files.associations": { - ".dapprc": "shellscript" + ".dapprc": "shellscript", + ".gas-snapshot": "julia" } } diff --git a/Makefile b/Makefile index 4c91a9a..6833b64 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ all: solc install update # Install proper solc version. -solc:; nix-env -f https://github.com/dapphub/dapptools/archive/master.tar.gz -iA solc-static-versions.solc_0_8_9 +solc:; nix-env -f https://github.com/dapphub/dapptools/archive/master.tar.gz -iA solc-static-versions.solc_0_8_10 # Install npm dependencies. install:; npm install # Install dapp dependencies. diff --git a/README.md b/README.md index 4899407..e330662 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,51 @@ -# vaults +# Vaults • [![Tests](https://github.com/Rari-Capital/vaults/actions/workflows/tests.yml/badge.svg)](https://github.com/Rari-Capital/vaults/actions/workflows/tests.yml) [![License](https://img.shields.io/badge/License-AGPL--3.0-blue)](LICENSE.md) -Flexible, minimalist, and gas-optimized yield aggregator protocol for earning interest on any ERC20 token. +Flexible, minimalist, and **gas-optimized yield aggregator protocol** for earning interest on any ERC20 token. -## Getting Started +- [Documentation](https://docs.rari.capital/vaults) +- [Deployments](https://github.com/Rari-Capital/vaults/releases) +- [Whitepaper](whitepaper/Whitepaper.pdf) +- [Audits](audits) + +## Architecture + +- [`Vault.sol`](src/Vault.sol): Flexible, minimalist, and gas-optimized yield aggregator for earning interest on any ERC20 token. +- [`VaultFactory.sol`](src/VaultFactory.sol): Factory which enables deploying a Vault contract for any ERC20 token. +- [`modules/`](src/modules): Contracts used for managing and/or simplifying interaction with Vaults and the Vault Factory. + - [`VaultRouterModule.sol`](src/modules/VaultRouterModule.sol): Module that enables depositing ETH and approval-free deposits via permit. + - [`VaultConfigurationModule.sol`](src/modules/VaultConfigurationModule.sol): Module for configuring Vault parameters. + - [`VaultInitializationModule.sol`](src/modules/VaultInitializationModule.sol): Module for initializing newly created Vaults. +- [`interfaces/`](src/interfaces): Interfaces of external contracts Vaults and modules interact with. + - [`Strategy.sol`](src/interfaces/Strategy.sol): Minimal interfaces for ERC20 and ETH compatible strategies. + +![Diagram](https://lucid.app/publicSegments/view/8069beaa-d2fc-48de-8f38-9e30edcb5f08/image.png) + +## Contributing + +You will need a copy of [DappTools](https://dapp.tools) installed before proceeding. See the [installation guide](https://github.com/dapphub/dapptools#installation) for details. + +### Setup ```sh git clone https://github.com/Rari-Capital/vaults.git cd vaults make ``` + +### Run Tests + +```sh +dapp test +``` + +### Measure Coverage + +```sh +dapp test --coverage +``` + +### Update Gas Snapshots + +```sh +dapp snapshot +``` diff --git a/audits/Fixed-Point-Solutions/Vault.pdf b/audits/Fixed-Point-Solutions/Vault.pdf new file mode 100644 index 0000000..29aee1f Binary files /dev/null and b/audits/Fixed-Point-Solutions/Vault.pdf differ diff --git a/audits/Fixed-Point-Solutions/VaultAuthorityModule.pdf b/audits/Fixed-Point-Solutions/VaultAuthorityModule.pdf new file mode 100644 index 0000000..e8f1026 Binary files /dev/null and b/audits/Fixed-Point-Solutions/VaultAuthorityModule.pdf differ diff --git a/audits/Fixed-Point-Solutions/VaultConfigurationModule.pdf b/audits/Fixed-Point-Solutions/VaultConfigurationModule.pdf new file mode 100644 index 0000000..598920c Binary files /dev/null and b/audits/Fixed-Point-Solutions/VaultConfigurationModule.pdf differ diff --git a/audits/Fixed-Point-Solutions/VaultInitializationModule.pdf b/audits/Fixed-Point-Solutions/VaultInitializationModule.pdf new file mode 100644 index 0000000..01607dd Binary files /dev/null and b/audits/Fixed-Point-Solutions/VaultInitializationModule.pdf differ diff --git a/audits/Fixed-Point-Solutions/VaultRouterModule.pdf b/audits/Fixed-Point-Solutions/VaultRouterModule.pdf new file mode 100644 index 0000000..3674409 Binary files /dev/null and b/audits/Fixed-Point-Solutions/VaultRouterModule.pdf differ diff --git a/audits/Quantstamp/Rari Vaults - Final Report.pdf b/audits/Quantstamp/Rari Vaults - Final Report.pdf new file mode 100644 index 0000000..92e23af Binary files /dev/null and b/audits/Quantstamp/Rari Vaults - Final Report.pdf differ diff --git a/audits/yAcademy/Dhurv.Kat.Amanusk.pdf b/audits/yAcademy/Dhurv.Kat.Amanusk.pdf new file mode 100644 index 0000000..d9e67c5 Binary files /dev/null and b/audits/yAcademy/Dhurv.Kat.Amanusk.pdf differ diff --git a/audits/yAcademy/Nibbler.Bebis.Zokunei.Carl.pdf b/audits/yAcademy/Nibbler.Bebis.Zokunei.Carl.pdf new file mode 100644 index 0000000..5b8f0ac Binary files /dev/null and b/audits/yAcademy/Nibbler.Bebis.Zokunei.Carl.pdf differ diff --git a/foundry.toml b/foundry.toml new file mode 100644 index 0000000..924368a --- /dev/null +++ b/foundry.toml @@ -0,0 +1,2 @@ +[default] +fuzz_runs = 1000 \ No newline at end of file diff --git a/lib/forge-std b/lib/forge-std new file mode 160000 index 0000000..8f1a972 --- /dev/null +++ b/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 8f1a9720250512a49c6638979a87613700e2a68b diff --git a/lib/solmate b/lib/solmate index 1f79c81..0a121c5 160000 --- a/lib/solmate +++ b/lib/solmate @@ -1 +1 @@ -Subproject commit 1f79c81e2b66f65af1c15c2df1140dc93157af30 +Subproject commit 0a121c54b23ccde77bb0dd84a59b71783c126894 diff --git a/scripts/contract-size.sh b/scripts/contract-size.sh new file mode 100755 index 0000000..e71e3aa --- /dev/null +++ b/scripts/contract-size.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +set -eo pipefail + +contract_size() { + NAME=$1 + ARGS=${@:2} + # select the filename and the contract in it + PATTERN=".contracts[\"src/$NAME.sol\"].$NAME" + + dapp build # first, build the contract + + # get the bytecode from the compiled file + BYTECODE=0x$(jq -r "$PATTERN.evm.bytecode.object" out/dapp.sol.json) + length=$(echo "$BYTECODE" | wc -m) + echo $(($length / 2)) +} + +if [[ -z $contract ]]; then + if [[ -z ${1} ]];then + echo '"$contract" env variable is not set. Set it to the name of the contract you want to estimate size for.' + exit 1 + else + contract=${1} + fi +fi + +contract_size=$(contract_size ${contract}) +echo "Contract Name: ${contract}" +echo "Contract Size: ${contract_size} bytes" +echo +echo "$(( 24576 - ${contract_size} )) bytes left to reach the smart contract size limit of 24576 bytes." diff --git a/shell.nix b/shell.nix index cf8fe21..834a642 100644 --- a/shell.nix +++ b/shell.nix @@ -2,7 +2,7 @@ let pkgs = import (builtins.fetchGit rec { name = "dapptools-${rev}"; url = https://github.com/dapphub/dapptools; - rev = "d7a23096d8ae8391e740f6bdc4e8b9b703ca4764"; + rev = "fb9476ded759da44c449eb391cc67bfb0df61112"; }) {}; in @@ -12,4 +12,4 @@ in buildInputs = with pkgs; [ pkgs.dapp ]; - } \ No newline at end of file + } diff --git a/src/Vault.sol b/src/Vault.sol index d2a48f6..456f81b 100644 --- a/src/Vault.sol +++ b/src/Vault.sol @@ -1,26 +1,34 @@ // SPDX-License-Identifier: AGPL-3.0-only -pragma solidity 0.8.9; +pragma solidity 0.8.10; import {Auth} from "solmate/auth/Auth.sol"; -import {WETH} from "solmate/tokens/WETH.sol"; -import {ERC20} from "solmate/tokens/ERC20.sol"; +import {ERC4626} from "solmate/mixins/ERC4626.sol"; + import {SafeCastLib} from "solmate/utils/SafeCastLib.sol"; import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; +import {WETH} from "solmate/tokens/WETH.sol"; +import {ERC20} from "solmate/tokens/ERC20.sol"; import {Strategy, ERC20Strategy, ETHStrategy} from "./interfaces/Strategy.sol"; -import {VaultFactory} from "./VaultFactory.sol"; - /// @title Rari Vault (rvToken) /// @author Transmissions11 and JetJadeja -/// @notice Minimalist yield aggregator designed to support any ERC20 token. -contract Vault is ERC20, Auth { +/// @notice Flexible, minimalist, and gas-optimized yield +/// aggregator for earning interest on any ERC20 token. +contract Vault is ERC4626, Auth { using SafeCastLib for uint256; - using SafeCastLib for uint128; using SafeTransferLib for ERC20; using FixedPointMathLib for uint256; + /*/////////////////////////////////////////////////////////////// + CONSTANTS + //////////////////////////////////////////////////////////////*/ + + /// @notice The maximum number of elements allowed on the withdrawal stack. + /// @dev Needed to prevent denial of service attacks by queue operators. + uint256 internal constant MAX_WITHDRAWAL_STACK_SIZE = 32; + /*/////////////////////////////////////////////////////////////// IMMUTABLES //////////////////////////////////////////////////////////////*/ @@ -30,27 +38,27 @@ contract Vault is ERC20, Auth { /// @notice The base unit of the underlying token and hence rvToken. /// @dev Equal to 10 ** decimals. Used for fixed point arithmetic. - uint256 public immutable BASE_UNIT; + uint256 internal immutable BASE_UNIT; /// @notice Creates a new Vault that accepts a specific underlying token. /// @param _UNDERLYING The ERC20 compliant token the Vault should accept. constructor(ERC20 _UNDERLYING) - ERC20( + ERC4626( + // Underlying token + _UNDERLYING, // ex: Rari Dai Stablecoin Vault string(abi.encodePacked("Rari ", _UNDERLYING.name(), " Vault")), // ex: rvDAI - string(abi.encodePacked("rv", _UNDERLYING.symbol())), - // ex: 18 - _UNDERLYING.decimals() + string(abi.encodePacked("rv", _UNDERLYING.symbol())) ) - Auth(VaultFactory(msg.sender).owner(), VaultFactory(msg.sender).authority()) + Auth(Auth(msg.sender).owner(), Auth(msg.sender).authority()) { UNDERLYING = _UNDERLYING; BASE_UNIT = 10**decimals; - // Prevent minting of fvTokens until - // the Vault is initialize is called. + // Prevent minting of rvTokens until + // the initialize function is called. totalSupply = type(uint256).max; } @@ -63,10 +71,11 @@ contract Vault is ERC20, Auth { uint256 public feePercent; /// @notice Emitted when the fee percentage is updated. - /// @param newFeePercent The updated fee percentage. - event FeePercentUpdated(uint256 newFeePercent); + /// @param user The authorized user who triggered the update. + /// @param newFeePercent The new fee percentage. + event FeePercentUpdated(address indexed user, uint256 newFeePercent); - /// @notice Set a new fee percentage. + /// @notice Sets a new fee percentage. /// @param newFeePercent The new fee percentage. function setFeePercent(uint256 newFeePercent) external requiresAuth { // A fee percentage over 100% doesn't make sense. @@ -75,28 +84,31 @@ contract Vault is ERC20, Auth { // Update the fee percentage. feePercent = newFeePercent; - emit FeePercentUpdated(newFeePercent); + emit FeePercentUpdated(msg.sender, newFeePercent); } /*/////////////////////////////////////////////////////////////// HARVEST CONFIGURATION //////////////////////////////////////////////////////////////*/ - //// @notice Emitted when the harvest window is updated. - //// @param newHarvestWindow The updated harvest window. - event HarvestWindowUpdated(uint128 newHarvestWindow); + /// @notice Emitted when the harvest window is updated. + /// @param user The authorized user who triggered the update. + /// @param newHarvestWindow The new harvest window. + event HarvestWindowUpdated(address indexed user, uint128 newHarvestWindow); /// @notice Emitted when the harvest delay is updated. - /// @param newHarvestDelay The updated harvest delay. - event HarvestDelayUpdated(uint64 newHarvestDelay); + /// @param user The authorized user who triggered the update. + /// @param newHarvestDelay The new harvest delay. + event HarvestDelayUpdated(address indexed user, uint64 newHarvestDelay); /// @notice Emitted when the harvest delay is scheduled to be updated next harvest. + /// @param user The authorized user who triggered the update. /// @param newHarvestDelay The scheduled updated harvest delay. - event HarvestDelayUpdateScheduled(uint64 newHarvestDelay); + event HarvestDelayUpdateScheduled(address indexed user, uint64 newHarvestDelay); /// @notice The period in seconds during which multiple harvests can occur /// regardless if they are taking place before the harvest delay has elapsed. - /// @dev Long harvest delays open up the Vault to profit distribution DOS attacks. + /// @dev Long harvest windows open the Vault up to profit distribution slowdown attacks. uint128 public harvestWindow; /// @notice The period in seconds over which locked profit is unlocked. @@ -107,7 +119,7 @@ contract Vault is ERC20, Auth { /// @dev In the case that the next delay is 0, no update will be applied. uint64 public nextHarvestDelay; - /// @notice Set a new harvest window. + /// @notice Sets a new harvest window. /// @param newHarvestWindow The new harvest window. /// @dev The Vault's harvestDelay must already be set before calling. function setHarvestWindow(uint128 newHarvestWindow) external requiresAuth { @@ -117,19 +129,19 @@ contract Vault is ERC20, Auth { // Update the harvest window. harvestWindow = newHarvestWindow; - emit HarvestWindowUpdated(newHarvestWindow); + emit HarvestWindowUpdated(msg.sender, newHarvestWindow); } - /// @notice Set a new harvest delay delay. + /// @notice Sets a new harvest delay. /// @param newHarvestDelay The new harvest delay to set. /// @dev If the current harvest delay is 0, meaning it has not - /// been set before, it will be updated immediately; otherwise + /// been set before, it will be updated immediately, otherwise /// it will be scheduled to take effect after the next harvest. function setHarvestDelay(uint64 newHarvestDelay) external requiresAuth { // A harvest delay of 0 makes harvests vulnerable to sandwich attacks. require(newHarvestDelay != 0, "DELAY_CANNOT_BE_ZERO"); - // A target harvest delay over 1 year doesn't make sense. + // A harvest delay longer than 1 year doesn't make sense. require(newHarvestDelay <= 365 days, "DELAY_TOO_LONG"); // If the harvest delay is 0, meaning it has not been set before: @@ -137,12 +149,12 @@ contract Vault is ERC20, Auth { // We'll apply the update immediately. harvestDelay = newHarvestDelay; - emit HarvestDelayUpdated(newHarvestDelay); + emit HarvestDelayUpdated(msg.sender, newHarvestDelay); } else { // We'll apply the update next harvest. nextHarvestDelay = newHarvestDelay; - emit HarvestDelayUpdateScheduled(newHarvestDelay); + emit HarvestDelayUpdateScheduled(msg.sender, newHarvestDelay); } } @@ -155,19 +167,20 @@ contract Vault is ERC20, Auth { uint256 public targetFloatPercent; /// @notice Emitted when the target float percentage is updated. - /// @param newTargetFloatPercent The updated target float percentage. - event TargetFloatPercentUpdated(uint256 newTargetFloatPercent); + /// @param user The authorized user who triggered the update. + /// @param newTargetFloatPercent The new target float percentage. + event TargetFloatPercentUpdated(address indexed user, uint256 newTargetFloatPercent); /// @notice Set a new target float percentage. /// @param newTargetFloatPercent The new target float percentage. function setTargetFloatPercent(uint256 newTargetFloatPercent) external requiresAuth { // A target float percentage over 100% doesn't make sense. - require(targetFloatPercent <= 1e18, "TARGET_TOO_HIGH"); + require(newTargetFloatPercent <= 1e18, "TARGET_TOO_HIGH"); // Update the target float percentage. targetFloatPercent = newTargetFloatPercent; - emit TargetFloatPercentUpdated(newTargetFloatPercent); + emit TargetFloatPercentUpdated(msg.sender, newTargetFloatPercent); } /*/////////////////////////////////////////////////////////////// @@ -179,20 +192,21 @@ contract Vault is ERC20, Auth { bool public underlyingIsWETH; /// @notice Emitted when whether the Vault should treat the underlying as WETH is updated. + /// @param user The authorized user who triggered the update. /// @param newUnderlyingIsWETH Whether the Vault nows treats the underlying as WETH. - event UnderlyingIsWETHUpdated(bool newUnderlyingIsWETH); + event UnderlyingIsWETHUpdated(address indexed user, bool newUnderlyingIsWETH); - /// @notice Set whether the Vault treats the underlying as WETH. + /// @notice Sets whether the Vault treats the underlying as WETH. /// @param newUnderlyingIsWETH Whether the Vault should treat the underlying as WETH. /// @dev The underlying token must have 18 decimals, to match Ether's decimal scheme. function setUnderlyingIsWETH(bool newUnderlyingIsWETH) external requiresAuth { - // Ensure the underlying token's decimals match ETH. - require(UNDERLYING.decimals() == 18, "WRONG_DECIMALS"); + // Ensure the underlying token's decimals match ETH if is WETH being set to true. + require(!newUnderlyingIsWETH || UNDERLYING.decimals() == 18, "WRONG_DECIMALS"); // Update whether the Vault treats the underlying as WETH. underlyingIsWETH = newUnderlyingIsWETH; - emit UnderlyingIsWETHUpdated(newUnderlyingIsWETH); + emit UnderlyingIsWETHUpdated(msg.sender, newUnderlyingIsWETH); } /*/////////////////////////////////////////////////////////////// @@ -231,137 +245,61 @@ contract Vault is ERC20, Auth { uint128 public maxLockedProfit; /*/////////////////////////////////////////////////////////////// - WITHDRAWAL QUEUE STORAGE + WITHDRAWAL STACK STORAGE //////////////////////////////////////////////////////////////*/ - /// @notice An ordered array of strategies representing the withdrawal queue. - /// @dev The queue is processed in descending order, meaning the last index will be withdrawn from first. - Strategy[] public withdrawalQueue; + /// @notice An ordered array of strategies representing the withdrawal stack. + /// @dev The stack is processed in descending order, meaning the last index will be withdrawn from first. + /// @dev Strategies that are untrusted, duplicated, or have no balance are filtered out when encountered at + /// withdrawal time, not validated upfront, meaning the stack may not reflect the "true" set used for withdrawals. + Strategy[] public withdrawalStack; - /// @notice Gets the full withdrawal queue. - /// @return An ordered array of strategies representing the withdrawal queue. + /// @notice Gets the full withdrawal stack. + /// @return An ordered array of strategies representing the withdrawal stack. /// @dev This is provided because Solidity converts public arrays into index getters, /// but we need a way to allow external contracts and users to access the whole array. - function getWithdrawalQueue() external view returns (Strategy[] memory) { - return withdrawalQueue; + function getWithdrawalStack() external view returns (Strategy[] memory) { + return withdrawalStack; } /*/////////////////////////////////////////////////////////////// DEPOSIT/WITHDRAWAL LOGIC //////////////////////////////////////////////////////////////*/ - /// @notice Emitted after a successful deposit. - /// @param user The address that deposited into the Vault. - /// @param underlyingAmount The amount of underlying tokens that were deposited. - event Deposit(address indexed user, uint256 underlyingAmount); - - /// @notice Emitted after a successful withdrawal. - /// @param user The address that withdrew from the Vault. - /// @param underlyingAmount The amount of underlying tokens that were withdrawn. - event Withdraw(address indexed user, uint256 underlyingAmount); - - /// @notice Deposit a specific amount of underlying tokens. - /// @param underlyingAmount The amount of the underlying token to deposit. - function deposit(uint256 underlyingAmount) external { - // We don't allow depositing 0 to prevent emitting a useless event. - require(underlyingAmount != 0, "AMOUNT_CANNOT_BE_ZERO"); + function afterDeposit(uint256, uint256) internal override {} - // Determine the equivalent amount of rvTokens and mint them. - _mint(msg.sender, underlyingAmount.fdiv(exchangeRate(), BASE_UNIT)); - - emit Deposit(msg.sender, underlyingAmount); - - // Transfer in underlying tokens from the user. - // This will revert if the user does not have the amount specified. - UNDERLYING.safeTransferFrom(msg.sender, address(this), underlyingAmount); + function beforeWithdraw(uint256 assets, uint256) internal override { + // Retrieve underlying tokens from strategies/float. + retrieveUnderlying(assets); } - /// @notice Withdraw a specific amount of underlying tokens. - /// @param underlyingAmount The amount of underlying tokens to withdraw. - function withdraw(uint256 underlyingAmount) external { - // We don't allow withdrawing 0 to prevent emitting a useless event. - require(underlyingAmount != 0, "AMOUNT_CANNOT_BE_ZERO"); - - // Determine the equivalent amount of rvTokens and burn them. - // This will revert if the user does not have enough rvTokens. - _burn(msg.sender, underlyingAmount.fdiv(exchangeRate(), BASE_UNIT)); - - emit Withdraw(msg.sender, underlyingAmount); - - // Withdraw from strategies if needed and transfer. - transferUnderlyingTo(msg.sender, underlyingAmount); - } - - /// @notice Redeem a specific amount of rvTokens for underlying tokens. - /// @param rvTokenAmount The amount of rvTokens to redeem for underlying tokens. - function redeem(uint256 rvTokenAmount) external { - // We don't allow redeeming 0 to prevent emitting a useless event. - require(rvTokenAmount != 0, "AMOUNT_CANNOT_BE_ZERO"); - - // Determine the equivalent amount of underlying tokens. - uint256 underlyingAmount = rvTokenAmount.fmul(exchangeRate(), BASE_UNIT); - - // Burn the provided amount of rvTokens. - // This will revert if the user does not have enough rvTokens. - _burn(msg.sender, rvTokenAmount); - - emit Withdraw(msg.sender, underlyingAmount); - - // Withdraw from strategies if needed and transfer. - transferUnderlyingTo(msg.sender, underlyingAmount); - } - - /// @dev Transfers a specific amount of underlying tokens held in strategies and/or float to a recipient. + /// @dev Retrieves a specific amount of underlying tokens held in strategies and/or float. /// @dev Only withdraws from strategies if needed and maintains the target float percentage if possible. - /// @param recipient The user to transfer the underlying tokens to. - /// @param underlyingAmount The amount of underlying tokens to transfer. - function transferUnderlyingTo(address recipient, uint256 underlyingAmount) internal { + /// @param underlyingAmount The amount of underlying tokens to retrieve. + function retrieveUnderlying(uint256 underlyingAmount) internal { // Get the Vault's floating balance. uint256 float = totalFloat(); // If the amount is greater than the float, withdraw from strategies. if (underlyingAmount > float) { - // Compute the bare minimum we need for this withdrawal. - uint256 floatDelta = underlyingAmount - float; - // Compute the amount needed to reach our target float percentage. - uint256 targetFloatDelta = (totalHoldings() - underlyingAmount).fmul(targetFloatPercent, 1e18); + uint256 floatMissingForTarget = (totalAssets() - underlyingAmount).mulWadDown(targetFloatPercent); - // Pull the desired amount from the withdrawal queue. - pullFromWithdrawalQueue((floatDelta + targetFloatDelta).safeCastTo224()); - } + // Compute the bare minimum amount we need for this withdrawal. + uint256 floatMissingForWithdrawal = underlyingAmount - float; - // Transfer the provided amount of underlying tokens. - UNDERLYING.safeTransfer(recipient, underlyingAmount); + // Pull enough to cover the withdrawal and reach our target float percentage. + pullFromWithdrawalStack(floatMissingForWithdrawal + floatMissingForTarget); + } } /*/////////////////////////////////////////////////////////////// VAULT ACCOUNTING LOGIC //////////////////////////////////////////////////////////////*/ - /// @notice Returns a user's Vault balance in underlying tokens. - /// @param user The user to get the underlying balance of. - /// @return The user's Vault balance in underlying tokens. - function balanceOfUnderlying(address user) external view returns (uint256) { - return balanceOf[user].fmul(exchangeRate(), BASE_UNIT); - } - - /// @notice Returns the amount of underlying tokens an rvToken can be redeemed for. - /// @return The amount of underlying tokens an rvToken can be redeemed for. - function exchangeRate() public view returns (uint256) { - // Get the total supply of rvTokens. - uint256 rvTokenSupply = totalSupply; - - // If there are no rvTokens in circulation, return an exchange rate of 1:1. - if (rvTokenSupply == 0) return BASE_UNIT; - - // Calculate the exchange rate by diving the total holdings by the rvToken supply. - return totalHoldings().fdiv(rvTokenSupply, BASE_UNIT); - } - - /// @notice Calculate the total amount of underlying tokens the Vault holds. + /// @notice Calculates the total amount of underlying tokens the Vault holds. /// @return totalUnderlyingHeld The total amount of underlying tokens the Vault holds. - function totalHoldings() public view returns (uint256 totalUnderlyingHeld) { + function totalAssets() public view override returns (uint256 totalUnderlyingHeld) { unchecked { // Cannot underflow as locked profit can't exceed total strategy holdings. totalUnderlyingHeld = totalStrategyHoldings - lockedProfit(); @@ -371,7 +309,7 @@ contract Vault is ERC20, Auth { totalUnderlyingHeld += totalFloat(); } - /// @notice Calculate the current amount of locked profit. + /// @notice Calculates the current amount of locked profit. /// @return The current amount of locked profit. function lockedProfit() public view returns (uint256) { // Get the last harvest and harvest delay. @@ -403,24 +341,19 @@ contract Vault is ERC20, Auth { //////////////////////////////////////////////////////////////*/ /// @notice Emitted after a successful harvest. - /// @param strategy The strategy that was harvested. - /// @param profitAccrued The amount of profit accrued by the harvest. - /// @param feesAccrued The amount of fees accrued during the harvest. - /// @dev If profitAccrued is 0 that could mean the strategy registered a loss. - event Harvest(Strategy indexed strategy, uint256 profitAccrued, uint256 feesAccrued); - - /// @notice Harvest a trusted strategy. - /// @param strategy The trusted strategy to harvest. - /// @dev Heavily optimized at the cost of some readability, as this function must - /// be called frequently by altruistic actors for the Vault to function as intended. - function harvest(Strategy strategy) external { - // If an untrusted strategy could be harvested a malicious user could use - // a fake strategy that over-reports holdings to manipulate the exchange rate. - require(getStrategyData[strategy].trusted, "UNTRUSTED_STRATEGY"); - + /// @param user The authorized user who triggered the harvest. + /// @param strategies The trusted strategies that were harvested. + event Harvest(address indexed user, Strategy[] strategies); + + /// @notice Harvest a set of trusted strategies. + /// @param strategies The trusted strategies to harvest. + /// @dev Will always revert if called outside of an active + /// harvest window or before the harvest delay has passed. + function harvest(Strategy[] calldata strategies) external requiresAuth { // If this is the first harvest after the last window: if (block.timestamp >= lastHarvest + harvestDelay) { // Set the harvest window's start timestamp. + // Cannot overflow 64 bits on human timescales. lastHarvestWindowStart = uint64(block.timestamp); } else { // We know this harvest is not the first in the window so we need to ensure it's within it. @@ -428,46 +361,62 @@ contract Vault is ERC20, Auth { } // Get the Vault's current total strategy holdings. - uint256 strategyHoldings = totalStrategyHoldings; + uint256 oldTotalStrategyHoldings = totalStrategyHoldings; + + // Used to store the total profit accrued by the strategies. + uint256 totalProfitAccrued; + + // Used to store the new total strategy holdings after harvesting. + uint256 newTotalStrategyHoldings = oldTotalStrategyHoldings; - // Get the strategy's previous and current balance. - uint256 balanceLastHarvest = getStrategyData[strategy].balance; - uint256 balanceThisHarvest = strategy.balanceOfUnderlying(address(this)); + // Will revert if any of the specified strategies are untrusted. + for (uint256 i = 0; i < strategies.length; i++) { + // Get the strategy at the current index. + Strategy strategy = strategies[i]; - // Compute the profit since last harvest. Will be 0 if it it had a net loss. - uint256 profitAccrued = balanceThisHarvest > balanceLastHarvest - ? balanceThisHarvest - balanceLastHarvest // Profits since last harvest. - : 0; // If the strategy registered a net loss we don't have any new profit. + // If an untrusted strategy could be harvested a malicious user could use + // a fake strategy that over-reports holdings to manipulate the exchange rate. + require(getStrategyData[strategy].trusted, "UNTRUSTED_STRATEGY"); + + // Get the strategy's previous and current balance. + uint256 balanceLastHarvest = getStrategyData[strategy].balance; + uint256 balanceThisHarvest = strategy.balanceOfUnderlying(address(this)); + + // Update the strategy's stored balance. Cast overflow is unrealistic. + getStrategyData[strategy].balance = balanceThisHarvest.safeCastTo248(); + + // Increase/decrease newTotalStrategyHoldings based on the profit/loss registered. + // We cannot wrap the subtraction in parenthesis as it would underflow if the strategy had a loss. + newTotalStrategyHoldings = newTotalStrategyHoldings + balanceThisHarvest - balanceLastHarvest; + + unchecked { + // Update the total profit accrued while counting losses as zero profit. + // Cannot overflow as we already increased total holdings without reverting. + totalProfitAccrued += balanceThisHarvest > balanceLastHarvest + ? balanceThisHarvest - balanceLastHarvest // Profits since last harvest. + : 0; // If the strategy registered a net loss we don't have any new profit. + } + } // Compute fees as the fee percent multiplied by the profit. - uint256 feesAccrued = profitAccrued.fmul(feePercent, 1e18); - - // If we accrued any fees, mint an equivalent amount of fvTokens. - // Authorized users can claim the newly minted fvTokens via claimFees. - if (feesAccrued != 0) - _mint( - address(this), - feesAccrued.fdiv( - // Optimized equivalent to exchangeRate. We don't subtract - // locked profit because it will always be 0 during a harvest. - (strategyHoldings + totalFloat()).fdiv(totalSupply, BASE_UNIT), - BASE_UNIT - ) - ); - - // Increase/decrease totalStrategyHoldings based on the profit/loss registered. - // We cannot wrap the subtraction in parenthesis as it would underflow if the strategy had a loss. - totalStrategyHoldings = strategyHoldings + balanceThisHarvest - balanceLastHarvest; - - // Update our stored balance for the strategy. - getStrategyData[strategy].balance = balanceThisHarvest.safeCastTo224(); - - // Update the max amount of locked profit - maxLockedProfit = (profitAccrued - feesAccrued).safeCastTo128(); + uint256 feesAccrued = totalProfitAccrued.mulDivDown(feePercent, 1e18); + + // If we accrued any fees, mint an equivalent amount of rvTokens. + // Authorized users can claim the newly minted rvTokens via claimFees. + _mint(address(this), feesAccrued.mulDivDown(BASE_UNIT, convertToAssets(BASE_UNIT))); + + // Update max unlocked profit based on any remaining locked profit plus new profit. + maxLockedProfit = (lockedProfit() + totalProfitAccrued - feesAccrued).safeCastTo128(); + + // Set strategy holdings to our new total. + totalStrategyHoldings = newTotalStrategyHoldings; // Update the last harvest timestamp. + // Cannot overflow on human timescales. lastHarvest = uint64(block.timestamp); + emit Harvest(msg.sender, strategies); + // Get the next harvest delay. uint64 newHarvestDelay = nextHarvestDelay; @@ -479,10 +428,8 @@ contract Vault is ERC20, Auth { // Reset the next harvest delay. nextHarvestDelay = 0; - emit HarvestDelayUpdated(newHarvestDelay); + emit HarvestDelayUpdated(msg.sender, newHarvestDelay); } - - emit Harvest(strategy, profitAccrued, feesAccrued); } /*/////////////////////////////////////////////////////////////// @@ -490,14 +437,16 @@ contract Vault is ERC20, Auth { //////////////////////////////////////////////////////////////*/ /// @notice Emitted after the Vault deposits into a strategy contract. + /// @param user The authorized user who triggered the deposit. /// @param strategy The strategy that was deposited into. /// @param underlyingAmount The amount of underlying tokens that were deposited. - event StrategyDeposit(Strategy indexed strategy, uint256 underlyingAmount); + event StrategyDeposit(address indexed user, Strategy indexed strategy, uint256 underlyingAmount); /// @notice Emitted after the Vault withdraws funds from a strategy contract. + /// @param user The authorized user who triggered the withdrawal. /// @param strategy The strategy that was withdrawn from. /// @param underlyingAmount The amount of underlying tokens that were withdrawn. - event StrategyWithdrawal(Strategy indexed strategy, uint256 underlyingAmount); + event StrategyWithdrawal(address indexed user, Strategy indexed strategy, uint256 underlyingAmount); /// @notice Deposit a specific amount of float into a trusted strategy. /// @param strategy The trusted strategy to deposit into. @@ -506,19 +455,16 @@ contract Vault is ERC20, Auth { // A strategy must be trusted before it can be deposited into. require(getStrategyData[strategy].trusted, "UNTRUSTED_STRATEGY"); - // We don't allow depositing 0 to prevent emitting a useless event. - require(underlyingAmount != 0, "AMOUNT_CANNOT_BE_ZERO"); - // Increase totalStrategyHoldings to account for the deposit. totalStrategyHoldings += underlyingAmount; unchecked { // Without this the next harvest would count the deposit as profit. // Cannot overflow as the balance of one strategy can't exceed the sum of all. - getStrategyData[strategy].balance += underlyingAmount.safeCastTo224(); + getStrategyData[strategy].balance += underlyingAmount.safeCastTo248(); } - emit StrategyDeposit(strategy, underlyingAmount); + emit StrategyDeposit(msg.sender, strategy, underlyingAmount); // We need to deposit differently if the strategy takes ETH. if (strategy.isCEther()) { @@ -539,16 +485,13 @@ contract Vault is ERC20, Auth { /// @notice Withdraw a specific amount of underlying tokens from a strategy. /// @param strategy The strategy to withdraw from. /// @param underlyingAmount The amount of underlying tokens to withdraw. - /// @dev Withdrawing from a strategy will not remove it from the withdrawal queue. + /// @dev Withdrawing from a strategy will not remove it from the withdrawal stack. function withdrawFromStrategy(Strategy strategy, uint256 underlyingAmount) external requiresAuth { // A strategy must be trusted before it can be withdrawn from. require(getStrategyData[strategy].trusted, "UNTRUSTED_STRATEGY"); - // We don't allow withdrawing 0 to prevent emitting a useless event. - require(underlyingAmount != 0, "AMOUNT_CANNOT_BE_ZERO"); - // Without this the next harvest would count the withdrawal as a loss. - getStrategyData[strategy].balance -= underlyingAmount.safeCastTo224(); + getStrategyData[strategy].balance -= underlyingAmount.safeCastTo248(); unchecked { // Decrease totalStrategyHoldings to account for the withdrawal. @@ -556,9 +499,9 @@ contract Vault is ERC20, Auth { totalStrategyHoldings -= underlyingAmount; } - emit StrategyWithdrawal(strategy, underlyingAmount); + emit StrategyWithdrawal(msg.sender, strategy, underlyingAmount); - // Withdraw from the strategy and revert if returns an error code. + // Withdraw from the strategy and revert if it returns an error code. require(strategy.redeemUnderlying(underlyingAmount) == 0, "REDEEM_FAILED"); // Wrap the withdrawn Ether into WETH if necessary. @@ -570,14 +513,16 @@ contract Vault is ERC20, Auth { //////////////////////////////////////////////////////////////*/ /// @notice Emitted when a strategy is set to trusted. + /// @param user The authorized user who trusted the strategy. /// @param strategy The strategy that became trusted. - event StrategyTrusted(Strategy indexed strategy); + event StrategyTrusted(address indexed user, Strategy indexed strategy); /// @notice Emitted when a strategy is set to untrusted. + /// @param user The authorized user who untrusted the strategy. /// @param strategy The strategy that became untrusted. - event StrategyDistrusted(Strategy indexed strategy); + event StrategyDistrusted(address indexed user, Strategy indexed strategy); - /// @notice Store a strategy as trusted, enabling it to be harvested. + /// @notice Stores a strategy as trusted, enabling it to be harvested. /// @param strategy The strategy to make trusted. function trustStrategy(Strategy strategy) external requiresAuth { // Ensure the strategy accepts the correct underlying token. @@ -590,114 +535,131 @@ contract Vault is ERC20, Auth { // Store the strategy as trusted. getStrategyData[strategy].trusted = true; - emit StrategyTrusted(strategy); + emit StrategyTrusted(msg.sender, strategy); } - /// @notice Store a strategy as untrusted, disabling it from being harvested. + /// @notice Stores a strategy as untrusted, disabling it from being harvested. /// @param strategy The strategy to make untrusted. function distrustStrategy(Strategy strategy) external requiresAuth { // Store the strategy as untrusted. getStrategyData[strategy].trusted = false; - emit StrategyDistrusted(strategy); + emit StrategyDistrusted(msg.sender, strategy); } /*/////////////////////////////////////////////////////////////// - WITHDRAWAL QUEUE LOGIC + WITHDRAWAL STACK LOGIC //////////////////////////////////////////////////////////////*/ - /// @notice Emitted when a strategy is pushed to the withdrawal queue. - /// @param pushedStrategy The strategy pushed to the withdrawal queue. - event WithdrawalQueuePushed(Strategy indexed pushedStrategy); - - /// @notice Emitted when a strategy is popped from the withdrawal queue. - /// @param poppedStrategy The strategy popped from the withdrawal queue. - event WithdrawalQueuePopped(Strategy indexed poppedStrategy); - - /// @notice Emitted when the withdrawal queue is updated. - /// @param replacedWithdrawalQueue The new withdrawal queue. - event WithdrawalQueueSet(Strategy[] replacedWithdrawalQueue); - - /// @notice Emitted when an index in the withdrawal queue is replaced. - /// @param index The index of the replaced strategy in the withdrawal queue. - /// @param replacedStrategy The strategy in the withdrawal queue that was replaced. + /// @notice Emitted when a strategy is pushed to the withdrawal stack. + /// @param user The authorized user who triggered the push. + /// @param pushedStrategy The strategy pushed to the withdrawal stack. + event WithdrawalStackPushed(address indexed user, Strategy indexed pushedStrategy); + + /// @notice Emitted when a strategy is popped from the withdrawal stack. + /// @param user The authorized user who triggered the pop. + /// @param poppedStrategy The strategy popped from the withdrawal stack. + event WithdrawalStackPopped(address indexed user, Strategy indexed poppedStrategy); + + /// @notice Emitted when the withdrawal stack is updated. + /// @param user The authorized user who triggered the set. + /// @param replacedWithdrawalStack The new withdrawal stack. + event WithdrawalStackSet(address indexed user, Strategy[] replacedWithdrawalStack); + + /// @notice Emitted when an index in the withdrawal stack is replaced. + /// @param user The authorized user who triggered the replacement. + /// @param index The index of the replaced strategy in the withdrawal stack. + /// @param replacedStrategy The strategy in the withdrawal stack that was replaced. /// @param replacementStrategy The strategy that overrode the replaced strategy at the index. - event WithdrawalQueueIndexReplaced( + event WithdrawalStackIndexReplaced( + address indexed user, uint256 index, Strategy indexed replacedStrategy, Strategy indexed replacementStrategy ); - /// @notice Emitted when an index in the withdrawal queue is replaced with the tip. - /// @param index The index of the replaced strategy in the withdrawal queue. - /// @param replacedStrategy The strategy in the withdrawal queue replaced by the tip. - /// @param previousTipStrategy The previous tip of the queue that replaced the strategy. - event WithdrawalQueueIndexReplacedWithTip( + /// @notice Emitted when an index in the withdrawal stack is replaced with the tip. + /// @param user The authorized user who triggered the replacement. + /// @param index The index of the replaced strategy in the withdrawal stack. + /// @param replacedStrategy The strategy in the withdrawal stack replaced by the tip. + /// @param previousTipStrategy The previous tip of the stack that replaced the strategy. + event WithdrawalStackIndexReplacedWithTip( + address indexed user, uint256 index, - Strategy replacedStrategy, + Strategy indexed replacedStrategy, Strategy indexed previousTipStrategy ); /// @notice Emitted when the strategies at two indexes are swapped. + /// @param user The authorized user who triggered the swap. /// @param index1 One index involved in the swap /// @param index2 The other index involved in the swap. /// @param newStrategy1 The strategy (previously at index2) that replaced index1. /// @param newStrategy2 The strategy (previously at index1) that replaced index2. - event WithdrawalQueueIndexesSwapped( + event WithdrawalStackIndexesSwapped( + address indexed user, uint256 index1, uint256 index2, Strategy indexed newStrategy1, Strategy indexed newStrategy2 ); - /// @dev Withdraw a specific amount of underlying tokens from strategies in the withdrawal queue. + /// @dev Withdraw a specific amount of underlying tokens from strategies in the withdrawal stack. /// @param underlyingAmount The amount of underlying tokens to pull into float. - /// @dev Automatically removes depleted strategies from the withdrawal queue. - function pullFromWithdrawalQueue(uint256 underlyingAmount) internal { + /// @dev Automatically removes depleted strategies from the withdrawal stack. + function pullFromWithdrawalStack(uint256 underlyingAmount) internal { // We will update this variable as we pull from strategies. uint256 amountLeftToPull = underlyingAmount; - // Store the starting index which is at the tip of the queue. - // Will revert due to underflow if there are no strategies in the queue. - uint256 startingIndex = withdrawalQueue.length - 1; - - // We will use this after the loop to check how many strategies we withdrew from. - uint256 currentIndex = startingIndex; + // We'll start at the tip of the stack and traverse backwards. + uint256 currentIndex = withdrawalStack.length - 1; - // Iterate in reverse so we pull from the queue in a "last in, first out" manner. - // Will revert due to underflow if we empty the queue before pulling the desired amount. + // Iterate in reverse so we pull from the stack in a "last in, first out" manner. + // Will revert due to underflow if we empty the stack before pulling the desired amount. for (; ; currentIndex--) { - // Get the strategy at the current queue index. - Strategy strategy = withdrawalQueue[currentIndex]; + // Get the strategy at the current stack index. + Strategy strategy = withdrawalStack[currentIndex]; // Get the balance of the strategy before we withdraw from it. uint256 strategyBalance = getStrategyData[strategy].balance; + // If the strategy is currently untrusted or was already depleted: + if (!getStrategyData[strategy].trusted || strategyBalance == 0) { + // Remove it from the stack. + withdrawalStack.pop(); + + emit WithdrawalStackPopped(msg.sender, strategy); + + // Move onto the next strategy. + continue; + } + // We want to pull as much as we can from the strategy, but no more than we need. - uint256 amountToPull = FixedPointMathLib.min(amountLeftToPull, strategyBalance); + uint256 amountToPull = strategyBalance > amountLeftToPull ? amountLeftToPull : strategyBalance; unchecked { // Compute the balance of the strategy that will remain after we withdraw. - // Cannot overflow as we cap the amount to pull at the strategy's balance. + // Cannot underflow as we cap the amount to pull at the strategy's balance. uint256 strategyBalanceAfterWithdrawal = strategyBalance - amountToPull; // Without this the next harvest would count the withdrawal as a loss. - getStrategyData[strategy].balance = strategyBalanceAfterWithdrawal.safeCastTo224(); + getStrategyData[strategy].balance = strategyBalanceAfterWithdrawal.safeCastTo248(); // Adjust our goal based on how much we can pull from the strategy. - // Cannot overflow as we cap the amount to pull at the amount left to pull. + // Cannot underflow as we cap the amount to pull at the amount left to pull. amountLeftToPull -= amountToPull; + emit StrategyWithdrawal(msg.sender, strategy, amountToPull); + // Withdraw from the strategy and revert if returns an error code. require(strategy.redeemUnderlying(amountToPull) == 0, "REDEEM_FAILED"); - emit StrategyWithdrawal(strategy, amountToPull); - - // If we depleted the strategy, pop it from the queue. + // If we fully depleted the strategy: if (strategyBalanceAfterWithdrawal == 0) { - withdrawalQueue.pop(); + // Remove it from the stack. + withdrawalStack.pop(); - emit WithdrawalQueuePopped(strategy); + emit WithdrawalStackPopped(msg.sender, strategy); } } @@ -707,7 +669,7 @@ contract Vault is ERC20, Auth { unchecked { // Account for the withdrawals done in the loop above. - // Cannot overflow as the balances of some strategies cannot exceed the sum of all. + // Cannot underflow as the balances of some strategies cannot exceed the sum of all. totalStrategyHoldings -= underlyingAmount; } @@ -718,110 +680,91 @@ contract Vault is ERC20, Auth { if (ethBalance != 0 && underlyingIsWETH) WETH(payable(address(UNDERLYING))).deposit{value: ethBalance}(); } - /// @notice Push a single strategy to front of the withdrawal queue. - /// @param strategy The strategy to be inserted at the front of the withdrawal queue. - function pushToWithdrawalQueue(Strategy strategy) external requiresAuth { - withdrawalQueue.push(strategy); + /// @notice Pushes a single strategy to front of the withdrawal stack. + /// @param strategy The strategy to be inserted at the front of the withdrawal stack. + /// @dev Strategies that are untrusted, duplicated, or have no balance are + /// filtered out when encountered at withdrawal time, not validated upfront. + function pushToWithdrawalStack(Strategy strategy) external requiresAuth { + // Ensure pushing the strategy will not cause the stack exceed its limit. + require(withdrawalStack.length < MAX_WITHDRAWAL_STACK_SIZE, "STACK_FULL"); + + // Push the strategy to the front of the stack. + withdrawalStack.push(strategy); - emit WithdrawalQueuePushed(strategy); + emit WithdrawalStackPushed(msg.sender, strategy); } - /// @notice Remove the strategy at the tip of the withdrawal queue. + /// @notice Removes the strategy at the tip of the withdrawal stack. /// @dev Be careful, another authorized user could push a different strategy - /// than expected to the queue while a popFromWithdrawalQueue transaction is pending. - function popFromWithdrawalQueue() external requiresAuth { + /// than expected to the stack while a popFromWithdrawalStack transaction is pending. + function popFromWithdrawalStack() external requiresAuth { // Get the (soon to be) popped strategy. - Strategy poppedStrategy = withdrawalQueue[withdrawalQueue.length - 1]; + Strategy poppedStrategy = withdrawalStack[withdrawalStack.length - 1]; - withdrawalQueue.pop(); + // Pop the first strategy in the stack. + withdrawalStack.pop(); - emit WithdrawalQueuePopped(poppedStrategy); + emit WithdrawalStackPopped(msg.sender, poppedStrategy); } - /// @notice Set the withdrawal queue. - /// @param newQueue The new withdrawal queue. - function setWithdrawalQueue(Strategy[] calldata newQueue) external requiresAuth { - withdrawalQueue = newQueue; + /// @notice Sets a new withdrawal stack. + /// @param newStack The new withdrawal stack. + /// @dev Strategies that are untrusted, duplicated, or have no balance are + /// filtered out when encountered at withdrawal time, not validated upfront. + function setWithdrawalStack(Strategy[] calldata newStack) external requiresAuth { + // Ensure the new stack is not larger than the maximum stack size. + require(newStack.length <= MAX_WITHDRAWAL_STACK_SIZE, "STACK_TOO_BIG"); + + // Replace the withdrawal stack. + withdrawalStack = newStack; - emit WithdrawalQueueSet(newQueue); + emit WithdrawalStackSet(msg.sender, newStack); } - /// @notice Replace an index in the withdrawal queue with another strategy. - /// @param index The index in the queue to replace. + /// @notice Replaces an index in the withdrawal stack with another strategy. + /// @param index The index in the stack to replace. /// @param replacementStrategy The strategy to override the index with. - function replaceWithdrawalQueueIndex(uint256 index, Strategy replacementStrategy) external { + /// @dev Strategies that are untrusted, duplicated, or have no balance are + /// filtered out when encountered at withdrawal time, not validated upfront. + function replaceWithdrawalStackIndex(uint256 index, Strategy replacementStrategy) external requiresAuth { // Get the (soon to be) replaced strategy. - Strategy replacedStrategy = withdrawalQueue[index]; + Strategy replacedStrategy = withdrawalStack[index]; - withdrawalQueue[index] = replacementStrategy; + // Update the index with the replacement strategy. + withdrawalStack[index] = replacementStrategy; - emit WithdrawalQueueIndexReplaced(index, replacedStrategy, replacementStrategy); + emit WithdrawalStackIndexReplaced(msg.sender, index, replacedStrategy, replacementStrategy); } - /// @notice Move the strategy at the tip of the queue to the specified index and pop the tip off the queue. - /// @param index The index of the strategy in the withdrawal queue to replace with the tip. - function replaceWithdrawalQueueIndexWithTip(uint256 index) external requiresAuth { + /// @notice Moves the strategy at the tip of the stack to the specified index and pop the tip off the stack. + /// @param index The index of the strategy in the withdrawal stack to replace with the tip. + function replaceWithdrawalStackIndexWithTip(uint256 index) external requiresAuth { // Get the (soon to be) previous tip and strategy we will replace at the index. - Strategy previousTipStrategy = withdrawalQueue[withdrawalQueue.length - 1]; - Strategy replacedStrategy = withdrawalQueue[index]; + Strategy previousTipStrategy = withdrawalStack[withdrawalStack.length - 1]; + Strategy replacedStrategy = withdrawalStack[index]; - // Replace the index specified with the tip of the queue. - withdrawalQueue[index] = previousTipStrategy; + // Replace the index specified with the tip of the stack. + withdrawalStack[index] = previousTipStrategy; // Remove the now duplicated tip from the array. - withdrawalQueue.pop(); + withdrawalStack.pop(); - emit WithdrawalQueueIndexReplacedWithTip(index, replacedStrategy, previousTipStrategy); + emit WithdrawalStackIndexReplacedWithTip(msg.sender, index, replacedStrategy, previousTipStrategy); } - /// @notice Swap two indexes in the withdrawal queue. + /// @notice Swaps two indexes in the withdrawal stack. /// @param index1 One index involved in the swap /// @param index2 The other index involved in the swap. - function swapWithdrawalQueueIndexes(uint256 index1, uint256 index2) external { + function swapWithdrawalStackIndexes(uint256 index1, uint256 index2) external requiresAuth { // Get the (soon to be) new strategies at each index. - Strategy newStrategy2 = withdrawalQueue[index1]; - Strategy newStrategy1 = withdrawalQueue[index2]; + Strategy newStrategy2 = withdrawalStack[index1]; + Strategy newStrategy1 = withdrawalStack[index2]; - withdrawalQueue[index1] = newStrategy1; - withdrawalQueue[index2] = newStrategy2; + // Swap the strategies at both indexes. + withdrawalStack[index1] = newStrategy1; + withdrawalStack[index2] = newStrategy2; - emit WithdrawalQueueIndexesSwapped(index1, index2, newStrategy1, newStrategy2); - } - - /*/////////////////////////////////////////////////////////////// - SEIZE STRATEGY LOGIC - //////////////////////////////////////////////////////////////*/ - - /// @notice Emitted after a strategy is seized. - /// @param strategy The strategy that was seized. - event StrategySeized(Strategy indexed strategy); - - /// @notice Seizes a strategy. - /// @param strategy The strategy to seize. - /// @dev Intended for use in emergencies or other extraneous situations where the - /// strategy requires interaction outside of the Vault's standard operating procedures. - function seizeStrategy(Strategy strategy) external requiresAuth { - // A strategy must be trusted before it can be seized. - require(getStrategyData[strategy].trusted, "UNTRUSTED_STRATEGY"); - - // Get balance the strategy last reported holding. - uint256 strategyBalance = getStrategyData[strategy].balance; - - // If the strategy's balance exceeds the Vault's total - // holdings, instantly unlock any remaining locked profit. - // Only necessary if the strategy has a lot of unlocked profit. - if (totalHoldings() >= strategyBalance) maxLockedProfit = 0; - - // Decrease the total by the strategy's balance. - totalStrategyHoldings - strategyBalance; - - // Set the strategy's balance to 0. - getStrategyData[strategy].balance = 0; - - emit StrategySeized(strategy); - - // Transfer all of the strategy's tokens to the caller. - ERC20(strategy).safeTransfer(msg.sender, strategy.balanceOf(address(this))); + emit WithdrawalStackIndexesSwapped(msg.sender, index1, index2, newStrategy1, newStrategy2); } /*/////////////////////////////////////////////////////////////// @@ -829,25 +772,27 @@ contract Vault is ERC20, Auth { //////////////////////////////////////////////////////////////*/ /// @notice Emitted after fees are claimed. - /// @param fvTokenAmount The amount of fvTokens that were claimed. - event FeesClaimed(uint256 fvTokenAmount); + /// @param user The authorized user who claimed the fees. + /// @param rvTokenAmount The amount of rvTokens that were claimed. + event FeesClaimed(address indexed user, uint256 rvTokenAmount); /// @notice Claims fees accrued from harvests. - /// @param fvTokenAmount The amount of fvTokens to claim. - /// @dev Accrued fees are measured as fvTokens held by the Vault. - function claimFees(uint256 fvTokenAmount) external requiresAuth { - // Transfer the provided amount of fvTokens to the caller. - ERC20(this).safeTransfer(msg.sender, fvTokenAmount); + /// @param rvTokenAmount The amount of rvTokens to claim. + /// @dev Accrued fees are measured as rvTokens held by the Vault. + function claimFees(uint256 rvTokenAmount) external requiresAuth { + emit FeesClaimed(msg.sender, rvTokenAmount); - emit FeesClaimed(fvTokenAmount); + // Transfer the provided amount of rvTokens to the caller. + ERC20(this).safeTransfer(msg.sender, rvTokenAmount); } /*/////////////////////////////////////////////////////////////// - INITIALIZATION LOGIC + INITIALIZATION AND DESTRUCTION LOGIC //////////////////////////////////////////////////////////////*/ /// @notice Emitted when the Vault is initialized. - event Initialized(); + /// @param user The authorized user who triggered the initialization. + event Initialized(address indexed user); /// @notice Whether the Vault has been initialized yet. /// @dev Can go from false to true, never from true to false. @@ -865,7 +810,13 @@ contract Vault is ERC20, Auth { // Open for deposits. totalSupply = 0; - emit Initialized(); + emit Initialized(msg.sender); + } + + /// @notice Self destructs a Vault, enabling it to be redeployed. + /// @dev Caller will receive any ETH held as float in the Vault. + function destroy() external requiresAuth { + selfdestruct(payable(msg.sender)); } /*/////////////////////////////////////////////////////////////// diff --git a/src/VaultFactory.sol b/src/VaultFactory.sol index 2b24465..eddd017 100644 --- a/src/VaultFactory.sol +++ b/src/VaultFactory.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: AGPL-3.0-only -pragma solidity 0.8.9; +pragma solidity 0.8.10; import {ERC20} from "solmate/tokens/ERC20.sol"; import {Auth, Authority} from "solmate/auth/Auth.sol"; @@ -9,24 +9,29 @@ import {Vault} from "./Vault.sol"; /// @title Rari Vault Factory /// @author Transmissions11 and JetJadeja -/// @notice Factory which enables deploying a Vault contract for any ERC20 token. -contract VaultFactory is Auth(msg.sender, Authority(address(0))) { +/// @notice Factory which enables deploying a Vault for any ERC20 token. +contract VaultFactory is Auth { using Bytes32AddressLib for address; using Bytes32AddressLib for bytes32; /*/////////////////////////////////////////////////////////////// - EVENTS + CONSTRUCTOR //////////////////////////////////////////////////////////////*/ - /// @notice Emitted when a new Vault is deployed. - /// @param vault The newly deployed Vault contract. - /// @param underlying The underlying token the new Vault accepts. - event VaultDeployed(Vault vault, ERC20 underlying); + /// @notice Creates a Vault factory. + /// @param _owner The owner of the factory. + /// @param _authority The Authority of the factory. + constructor(address _owner, Authority _authority) Auth(_owner, _authority) {} /*/////////////////////////////////////////////////////////////// VAULT DEPLOYMENT LOGIC //////////////////////////////////////////////////////////////*/ + /// @notice Emitted when a new Vault is deployed. + /// @param vault The newly deployed Vault contract. + /// @param underlying The underlying token the new Vault accepts. + event VaultDeployed(Vault vault, ERC20 underlying); + /// @notice Deploys a new Vault which supports a specific underlying token. /// @dev This will revert if a Vault that accepts the same underlying token has already been deployed. /// @param underlying The ERC20 token that the Vault should accept. diff --git a/src/interfaces/AllowedPermit.sol b/src/interfaces/AllowedPermit.sol new file mode 100644 index 0000000..04a9db9 --- /dev/null +++ b/src/interfaces/AllowedPermit.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.10; + +import {ERC20} from "solmate/tokens/ERC20.sol"; + +/// @notice Minimal interface for tokens using DAI's non-standard permit interface. +/// @author Modified from Uniswap (https://github.com/Uniswap/v3-periphery/blob/main/contracts/interfaces/external/IERC20PermitAllowed.sol) +abstract contract AllowedPermit is ERC20 { + /// @param holder The address of the token owner. + /// @param spender The address of the token spender. + /// @param nonce The owner's nonce, increases at each call to permit. + /// @param expiry The timestamp at which the permit is no longer valid. + /// @param allowed Boolean that sets approval amount, true for type(uint256).max and false for 0. + /// @param v Must produce valid secp256k1 signature from the owner along with r and s. + /// @param r Must produce valid secp256k1 signature from the owner along with v and s. + /// @param s Must produce valid secp256k1 signature from the owner along with r and v. + function permit( + address holder, + address spender, + uint256 nonce, + uint256 expiry, + bool allowed, + uint8 v, + bytes32 r, + bytes32 s + ) external virtual; +} diff --git a/src/interfaces/Strategy.sol b/src/interfaces/Strategy.sol index dfdfdf1..3090dc9 100644 --- a/src/interfaces/Strategy.sol +++ b/src/interfaces/Strategy.sol @@ -1,22 +1,46 @@ // SPDX-License-Identifier: AGPL-3.0-only -pragma solidity 0.8.9; +pragma solidity 0.8.10; import {ERC20} from "solmate/tokens/ERC20.sol"; +/// @notice Minimal interface for Vault compatible strategies. +/// @dev Designed for out of the box compatibility with Fuse cTokens. +/// @dev Like cTokens, strategies must be transferrable ERC20s. abstract contract Strategy is ERC20 { + /// @notice Returns whether the strategy accepts ETH or an ERC20. + /// @return True if the strategy accepts ETH, false otherwise. + /// @dev Only present in Fuse cTokens, not Compound cTokens. function isCEther() external view virtual returns (bool); + /// @notice Withdraws a specific amount of underlying tokens from the strategy. + /// @param amount The amount of underlying tokens to withdraw. + /// @return An error code, or 0 if the withdrawal was successful. function redeemUnderlying(uint256 amount) external virtual returns (uint256); + /// @notice Returns a user's strategy balance in underlying tokens. + /// @param user The user to get the underlying balance of. + /// @return The user's strategy balance in underlying tokens. + /// @dev May mutate the state of the strategy by accruing interest. function balanceOfUnderlying(address user) external virtual returns (uint256); } +/// @notice Minimal interface for Vault strategies that accept ERC20s. +/// @dev Designed for out of the box compatibility with Fuse cERC20s. abstract contract ERC20Strategy is Strategy { + /// @notice Returns the underlying ERC20 token the strategy accepts. + /// @return The underlying ERC20 token the strategy accepts. function underlying() external view virtual returns (ERC20); + /// @notice Deposit a specific amount of underlying tokens into the strategy. + /// @param amount The amount of underlying tokens to deposit. + /// @return An error code, or 0 if the deposit was successful. function mint(uint256 amount) external virtual returns (uint256); } +/// @notice Minimal interface for Vault strategies that accept ETH. +/// @dev Designed for out of the box compatibility with Fuse cEther. abstract contract ETHStrategy is Strategy { + /// @notice Deposit a specific amount of ETH into the strategy. + /// @dev The amount of ETH is specified via msg.value. Reverts on error. function mint() external payable virtual; } diff --git a/src/modules/VaultAuthorityModule.sol b/src/modules/VaultAuthorityModule.sol index eefe7d0..c0c69f1 100644 --- a/src/modules/VaultAuthorityModule.sol +++ b/src/modules/VaultAuthorityModule.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: AGPL-3.0-only -pragma solidity 0.8.9; +pragma solidity 0.8.10; import {Auth, Authority} from "solmate/auth/Auth.sol"; @@ -7,25 +7,13 @@ import {Auth, Authority} from "solmate/auth/Auth.sol"; /// @notice Module for managing access to secured Vault operations. /// @author Modified from Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/auth/authorities/RolesAuthority.sol) contract VaultAuthorityModule is Auth, Authority { - /*/////////////////////////////////////////////////////////////// - EVENTS - //////////////////////////////////////////////////////////////*/ - - event TargetCustomAuthorityUpdated(address indexed target, Authority indexed authority); - - event UserRootUpdated(address indexed user, bool enabled); - - event UserRoleUpdated(address indexed user, uint8 indexed role, bool enabled); - - event RoleCapabilityUpdated(uint8 indexed role, bytes4 indexed functionSig, bool enabled); - /*/////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ /// @notice Creates a Vault configuration module. /// @param _owner The owner of the module. - /// @param _authority The authority of the module. + /// @param _authority The Authority of the module. constructor(address _owner, Authority _authority) Auth(_owner, _authority) {} /*/////////////////////////////////////////////////////////////// @@ -33,25 +21,15 @@ contract VaultAuthorityModule is Auth, Authority { //////////////////////////////////////////////////////////////*/ /// @notice Maps targets to a custom Authority to use for authorization. - mapping(address => Authority) public getCustomAuthority; + mapping(address => Authority) public getTargetCustomAuthority; /*/////////////////////////////////////////////////////////////// USER ROLE STORAGE //////////////////////////////////////////////////////////////*/ - /// @notice Maps users to a boolean indicating whether they have root access. - mapping(address => bool) public isUserRoot; - /// @notice Maps users to a bytes32 set of all the roles assigned to them. mapping(address => bytes32) public getUserRoles; - /*/////////////////////////////////////////////////////////////// - ROLE CAPABILITY STORAGE - //////////////////////////////////////////////////////////////*/ - - /// @dev Maps function signature to a set of all roles that can call the given function. - mapping(bytes4 => bytes32) public getRoleCapabilities; - /// @notice Gets whether a user has a specific role. /// @param user The user to check for. /// @param role The role to check if the user has. @@ -66,45 +44,72 @@ contract VaultAuthorityModule is Auth, Authority { } } - /// @notice Returns if a user can call a given Vault's function. + /*/////////////////////////////////////////////////////////////// + ROLE CAPABILITY STORAGE + //////////////////////////////////////////////////////////////*/ + + /// @notice Maps function signatures to a set of all roles that can call the given function. + mapping(bytes4 => bytes32) public getRoleCapabilities; + + /// @notice Maps function signatures to a boolean indicating whether anyone can call the given function. + mapping(bytes4 => bool) public isCapabilityPublic; + + /// @notice Gets whether a role has a specific capability. + /// @param role The role to check for. + /// @param functionSig function to check the role is capable of calling. + /// @return A boolean indicating whether the role has the capability. + function doesRoleHaveCapability(uint8 role, bytes4 functionSig) external view virtual returns (bool) { + unchecked { + // Generate a mask for the role. + bytes32 shifted = bytes32(uint256(uint256(2)**uint256(role))); + + // Check if the role has the capability using the generated mask. + return bytes32(0) != getRoleCapabilities[functionSig] & shifted; + } + } + + /*/////////////////////////////////////////////////////////////// + AUTHORIZATION LOGIC + //////////////////////////////////////////////////////////////*/ + + /// @notice Returns if a user can call a given target's function. /// @param user The user to check for. - /// @param target The Vault the user is trying to call. + /// @param target The target the user is trying to call. /// @param functionSig The function signature the user is trying to call. - /// @return A boolean indicating if the user can call the function on the Vault. - /// @dev First checks if the user is authorized to call all Vault's with the given function. - /// If they are not it then checks if the Vault has a custom Authority. If so it returns whether - /// it the user is authorized to call the function, otherwise execution ends and it returns false. + /// @return A boolean indicating if the user can call the function on the target. + /// @dev First checks whether the target has a custom Authority assigned to it, if so returns + /// whether the custom Authority would allow the user to call the desired function on the target, + /// otherwise returns whether the user is able to call the desired function on any target contract. function canCall( address user, address target, bytes4 functionSig ) external view override returns (bool) { - // Get the user's role set. - bytes32 userRoles = getUserRoles[user]; - - // Get the set of roles authorized to call the function. - bytes32 rolesAuthorized = getRoleCapabilities[functionSig]; - - // Check if the user has an authorized role or is root and return true if so. - if (bytes32(0) != userRoles & rolesAuthorized || isUserRoot[user]) return true; + // Get the target's custom Authority. Will be address(0) if none. + Authority customAuthority = getTargetCustomAuthority[target]; - // Get the target's custom Authority. - Authority customAuthority = getCustomAuthority[target]; + // If a custom Authority is set, return whether the Authority allows the user to call the function. + if (address(customAuthority) != address(0)) return customAuthority.canCall(user, target, functionSig); - // If a custom authority is set, return whether the Authority allows the user to call the function. - return address(customAuthority) != address(0) && customAuthority.canCall(user, target, functionSig); + // Return whether the user has an authorized role or the capability is publicly accessible. + return bytes32(0) != getUserRoles[user] & getRoleCapabilities[functionSig] || isCapabilityPublic[functionSig]; } /*/////////////////////////////////////////////////////////////// CUSTOM TARGET AUTHORITY CONFIGURATION LOGIC //////////////////////////////////////////////////////////////*/ + /// @notice Emitted when a custom Authority is set for a target. + /// @param target The target who had a custom Authority set. + /// @param authority The custom Authority set for the target. + event TargetCustomAuthorityUpdated(address indexed target, Authority indexed authority); + /// @notice Sets a custom Authority for a target. /// @param target The target to set a custom Authority for. /// @param customAuthority The custom Authority to set. function setTargetCustomAuthority(address target, Authority customAuthority) external requiresAuth { // Update the target's custom Authority. - getCustomAuthority[target] = customAuthority; + getTargetCustomAuthority[target] = customAuthority; emit TargetCustomAuthorityUpdated(target, customAuthority); } @@ -113,6 +118,12 @@ contract VaultAuthorityModule is Auth, Authority { ROLE CAPABILITY CONFIGURATION LOGIC //////////////////////////////////////////////////////////////*/ + /// @notice Emitted when a role's capabilities are updated. + /// @param role The role whose capabilities were updated. + /// @param functionSig The function the role was enabled to call or not. + /// @param enabled Whether the role is now able to call the function or not. + event RoleCapabilityUpdated(uint8 indexed role, bytes4 indexed functionSig, bool enabled); + /// @notice Sets a capability for a role. /// @param role The role to set a capability for. /// @param functionSig The function to enable the role to call or not. @@ -122,24 +133,49 @@ contract VaultAuthorityModule is Auth, Authority { bytes4 functionSig, bool enabled ) external requiresAuth { - // Get the previous role capability set. - bytes32 lastRoles = getRoleCapabilities[functionSig]; + // Get the previous set of role capabilities. + bytes32 lastCapabilities = getRoleCapabilities[functionSig]; unchecked { // Generate a mask for the role. bytes32 shifted = bytes32(uint256(uint256(2)**uint256(role))); // Update the role's capability set with the role mask. - getRoleCapabilities[functionSig] = enabled ? lastRoles | shifted : lastRoles & ~shifted; + getRoleCapabilities[functionSig] = enabled ? lastCapabilities | shifted : lastCapabilities & ~shifted; } emit RoleCapabilityUpdated(role, functionSig, enabled); } + /*/////////////////////////////////////////////////////////////// + PUBLIC CAPABILITY CONFIGURATION LOGIC + //////////////////////////////////////////////////////////////*/ + + /// @notice Emitted when whether a capability is public is updated. + /// @param functionSig The function that was made public or not. + /// @param enabled Whether the function is not publicly callable or not. + event PublicCapabilityUpdated(bytes4 indexed functionSig, bool enabled); + + /// @notice Sets whether a capability is public or not. + /// @param functionSig The function make public or not. + /// @param enabled Whether the function should be public or not. + function setPublicCapability(bytes4 functionSig, bool enabled) external requiresAuth { + // Update whether the capability is public. + isCapabilityPublic[functionSig] = enabled; + + emit PublicCapabilityUpdated(functionSig, enabled); + } + /*/////////////////////////////////////////////////////////////// USER ROLE ASSIGNMENT LOGIC //////////////////////////////////////////////////////////////*/ + /// @notice Emitted when a user's role is updated. + /// @param user The user who had their role updated. + /// @param role The role the user had assigned/removed. + /// @param enabled Whether the user had the role assigned/removed. + event UserRoleUpdated(address indexed user, uint8 indexed role, bool enabled); + /// @notice Assigns a role to a user. /// @param user The user to assign a role to. /// @param role The role to assign to the user. @@ -162,14 +198,4 @@ contract VaultAuthorityModule is Auth, Authority { emit UserRoleUpdated(user, role, enabled); } - - /// @notice Sets a user as a root user. - /// @param user The user to set as a root user. - /// @param enabled Whether the user should be a root user or not. - function setRootUser(address user, bool enabled) external requiresAuth { - // Update whether the user is a root user. - isUserRoot[user] = enabled; - - emit UserRootUpdated(user, enabled); - } } diff --git a/src/modules/VaultConfigurationModule.sol b/src/modules/VaultConfigurationModule.sol index 96da241..f66f5af 100644 --- a/src/modules/VaultConfigurationModule.sol +++ b/src/modules/VaultConfigurationModule.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: AGPL-3.0-only -pragma solidity 0.8.9; +pragma solidity 0.8.10; import {Auth, Authority} from "solmate/auth/Auth.sol"; @@ -15,9 +15,85 @@ contract VaultConfigurationModule is Auth { /// @notice Creates a Vault configuration module. /// @param _owner The owner of the module. - /// @param _authority The authority of the module. + /// @param _authority The Authority of the module. constructor(address _owner, Authority _authority) Auth(_owner, _authority) {} + /*/////////////////////////////////////////////////////////////// + DEFAULT VAULT PARAMETER CONFIGURATION + //////////////////////////////////////////////////////////////*/ + + /// @notice Emitted when the default fee percentage is updated. + /// @param newDefaultFeePercent The new default fee percentage. + event DefaultFeePercentUpdated(uint256 newDefaultFeePercent); + + /// @notice Emitted when the default harvest delay is updated. + /// @param newDefaultHarvestDelay The new default harvest delay. + event DefaultHarvestDelayUpdated(uint64 newDefaultHarvestDelay); + + /// @notice Emitted when the default harvest window is updated. + /// @param newDefaultHarvestWindow The new default harvest window. + event DefaultHarvestWindowUpdated(uint128 newDefaultHarvestWindow); + + /// @notice Emitted when the default target float percentage is updated. + /// @param newDefaultTargetFloatPercent The new default target float percentage. + event DefaultTargetFloatPercentUpdated(uint256 newDefaultTargetFloatPercent); + + /// @notice The default fee percentage for Vaults. + /// @dev See the documentation for the feePercentage + /// variable in the Vault contract for more details. + uint256 public defaultFeePercent; + + /// @notice The default harvest delay for Vaults. + /// @dev See the documentation for the harvestDelay + /// variable in the Vault contract for more details. + uint64 public defaultHarvestDelay; + + /// @notice The default harvest window for Vaults. + /// @dev See the documentation for the harvestWindow + /// variable in the Vault contract for more details. + uint128 public defaultHarvestWindow; + + /// @notice The default target float percentage for Vaults. + /// @dev See the documentation for the targetFloatPercent + /// variable in the Vault contract for more details. + uint256 public defaultTargetFloatPercent; + + /// @notice Sets the default fee percentage for Vaults. + /// @param newDefaultFeePercent The new default fee percentage to set. + function setDefaultFeePercent(uint256 newDefaultFeePercent) external requiresAuth { + // Update the default fee percentage. + defaultFeePercent = newDefaultFeePercent; + + emit DefaultFeePercentUpdated(newDefaultFeePercent); + } + + /// @notice Sets the default harvest delay for Vaults. + /// @param newDefaultHarvestDelay The new default harvest delay to set. + function setDefaultHarvestDelay(uint64 newDefaultHarvestDelay) external requiresAuth { + // Update the default harvest delay. + defaultHarvestDelay = newDefaultHarvestDelay; + + emit DefaultHarvestDelayUpdated(newDefaultHarvestDelay); + } + + /// @notice Sets the default harvest window for Vaults. + /// @param newDefaultHarvestWindow The new default harvest window to set. + function setDefaultHarvestWindow(uint128 newDefaultHarvestWindow) external requiresAuth { + // Update the default harvest window. + defaultHarvestWindow = newDefaultHarvestWindow; + + emit DefaultHarvestWindowUpdated(newDefaultHarvestWindow); + } + + /// @notice Sets the default target float percentage for Vaults. + /// @param newDefaultTargetFloatPercent The new default target float percentage to set. + function setDefaultTargetFloatPercent(uint256 newDefaultTargetFloatPercent) external requiresAuth { + // Update the default target float percentage. + defaultTargetFloatPercent = newDefaultTargetFloatPercent; + + emit DefaultTargetFloatPercentUpdated(newDefaultTargetFloatPercent); + } + /*/////////////////////////////////////////////////////////////// CUSTOM VAULT PARAMETER CONFIGURATION //////////////////////////////////////////////////////////////*/ @@ -44,19 +120,23 @@ contract VaultConfigurationModule is Auth { /// @notice Maps Vaults to their custom fee percentage. /// @dev Will be 0 if there is no custom fee percentage for the Vault. - mapping(Vault => uint256) getVaultCustomFeePercent; + /// @dev See the documentation for the targetFloatPercent variable in the Vault contract for more details. + mapping(Vault => uint256) public getVaultCustomFeePercent; /// @notice Maps Vaults to their custom harvest delay. /// @dev Will be 0 if there is no custom harvest delay for the Vault. - mapping(Vault => uint64) getVaultCustomHarvestDelay; + /// @dev See the documentation for the harvestDelay variable in the Vault contract for more details. + mapping(Vault => uint64) public getVaultCustomHarvestDelay; /// @notice Maps Vaults to their custom harvest window. /// @dev Will be 0 if there is no custom harvest window for the Vault. - mapping(Vault => uint128) getVaultCustomHarvestWindow; + /// @dev See the documentation for the harvestWindow variable in the Vault contract for more details. + mapping(Vault => uint128) public getVaultCustomHarvestWindow; /// @notice Maps Vaults to their custom target float percentage. /// @dev Will be 0 if there is no custom target float percentage for the Vault. - mapping(Vault => uint256) getVaultCustomTargetFloatPercent; + /// @dev See the documentation for the targetFloatPercent variable in the Vault contract for more details. + mapping(Vault => uint256) public getVaultCustomTargetFloatPercent; /// @notice Sets the custom fee percentage for the Vault. /// @param vault The Vault to set the custom fee percentage for. @@ -98,74 +178,6 @@ contract VaultConfigurationModule is Auth { emit CustomTargetFloatPercentUpdated(vault, customTargetFloatPercent); } - /*/////////////////////////////////////////////////////////////// - DEFAULT VAULT PARAMETER CONFIGURATION - //////////////////////////////////////////////////////////////*/ - - /// @notice Emitted when the default fee percentage is updated. - /// @param newDefaultFeePercent The new default fee percentage. - event DefaultFeePercentUpdated(uint256 newDefaultFeePercent); - - /// @notice Emitted when the default harvest delay is updated. - /// @param newDefaultHarvestDelay The new default harvest delay. - event DefaultHarvestDelayUpdated(uint64 newDefaultHarvestDelay); - - /// @notice Emitted when the default harvest window is updated. - /// @param newDefaultHarvestWindow The new default harvest window. - event DefaultHarvestWindowUpdated(uint128 newDefaultHarvestWindow); - - /// @notice Emitted when the default target float percentage is updated. - /// @param newDefaultTargetFloatPercent The new default target float percentage. - event DefaultTargetFloatPercentUpdated(uint256 newDefaultTargetFloatPercent); - - /// @notice The default fee percentage for Vaults. - uint256 public defaultFeePercent; - - /// @notice The default harvest delay for Vaults. - uint64 public defaultHarvestDelay; - - /// @notice The default harvest window for Vaults. - uint128 public defaultHarvestWindow; - - /// @notice The default target float percentage for Vaults. - uint256 public defaultTargetFloatPercent; - - /// @notice Sets the default fee percentage for Vaults. - /// @param newDefaultFeePercent The new default fee percentage to set. - function setDefaultFeePercent(uint256 newDefaultFeePercent) external requiresAuth { - // Update the default fee percentage. - defaultFeePercent = newDefaultFeePercent; - - emit DefaultFeePercentUpdated(newDefaultFeePercent); - } - - /// @notice Sets the default harvest delay for Vaults. - /// @param newDefaultHarvestDelay The new default harvest delay to set. - function setDefaultHarvestDelay(uint64 newDefaultHarvestDelay) external requiresAuth { - // Update the default harvest delay. - defaultHarvestDelay = newDefaultHarvestDelay; - - emit DefaultHarvestDelayUpdated(newDefaultHarvestDelay); - } - - /// @notice Sets the default harvest window for Vaults. - /// @param newDefaultHarvestWindow The new default harvest window to set. - function setDefaultHarvestWindow(uint128 newDefaultHarvestWindow) external requiresAuth { - // Update the default harvest window. - defaultHarvestWindow = newDefaultHarvestWindow; - - emit DefaultHarvestWindowUpdated(newDefaultHarvestWindow); - } - - /// @notice Sets the default target float percentage for Vaults. - /// @param newDefaultTargetFloatPercent The new default target float percentage to set. - function setDefaultTargetFloatPercent(uint256 newDefaultTargetFloatPercent) external requiresAuth { - // Update the default target float percentage. - defaultTargetFloatPercent = newDefaultTargetFloatPercent; - - emit DefaultTargetFloatPercentUpdated(newDefaultTargetFloatPercent); - } - /*/////////////////////////////////////////////////////////////// VAULT PARAMETER SYNC LOGIC //////////////////////////////////////////////////////////////*/ @@ -177,9 +189,15 @@ contract VaultConfigurationModule is Auth { // Get the Vault's custom fee percentage. uint256 customFeePercent = getVaultCustomFeePercent[vault]; + // Determine what the new fee percentage should be for the Vault after the sync. + uint256 newFeePercent = customFeePercent == 0 ? defaultFeePercent : customFeePercent; + + // Prevent spamming as this function requires no authorization. + require(vault.feePercent() != newFeePercent, "ALREADY_SYNCED"); + // Set the Vault's fee percentage to the custom fee percentage // or the default fee percentage if a custom percentage isn't set. - vault.setFeePercent(customFeePercent == 0 ? defaultFeePercent : customFeePercent); + vault.setFeePercent(newFeePercent); } /// @notice Syncs a Vault's harvest delay with either the Vault's custom @@ -189,9 +207,15 @@ contract VaultConfigurationModule is Auth { // Get the Vault's custom harvest delay. uint64 customHarvestDelay = getVaultCustomHarvestDelay[vault]; + // Determine what the new harvest delay should be for the Vault after the sync. + uint64 newHarvestDelay = customHarvestDelay == 0 ? defaultHarvestDelay : customHarvestDelay; + + // Prevent spamming as this function requires no authorization. + require(vault.harvestDelay() != newHarvestDelay, "ALREADY_SYNCED"); + // Set the Vault's harvest delay to the custom harvest delay // or the default harvest delay if a custom delay isn't set. - vault.setHarvestDelay(customHarvestDelay == 0 ? defaultHarvestDelay : customHarvestDelay); + vault.setHarvestDelay(newHarvestDelay); } /// @notice Syncs a Vault's harvest window with either the Vault's custom @@ -201,9 +225,15 @@ contract VaultConfigurationModule is Auth { // Get the Vault's custom harvest window. uint128 customHarvestWindow = getVaultCustomHarvestWindow[vault]; + // Determine what the new harvest window should be for the Vault after the sync. + uint128 newHarvestWindow = customHarvestWindow == 0 ? defaultHarvestWindow : customHarvestWindow; + + // Prevent spamming as this function requires no authorization. + require(vault.harvestWindow() != newHarvestWindow, "ALREADY_SYNCED"); + // Set the Vault's harvest window to the custom harvest window // or the default harvest window if a custom window isn't set. - vault.setHarvestWindow(customHarvestWindow == 0 ? defaultHarvestWindow : customHarvestWindow); + vault.setHarvestWindow(newHarvestWindow); } /// @notice Syncs a Vault's target float percentage with either the Vault's custom target @@ -213,10 +243,16 @@ contract VaultConfigurationModule is Auth { // Get the Vault's custom target float percentage. uint256 customTargetFloatPercent = getVaultCustomTargetFloatPercent[vault]; + // Determine what the new target float percentage should be for the Vault after the sync. + uint256 newTargetFloatPercent = customTargetFloatPercent == 0 + ? defaultTargetFloatPercent + : customTargetFloatPercent; + + // Prevent spamming as this function requires no authorization. + require(vault.targetFloatPercent() != newTargetFloatPercent, "ALREADY_SYNCED"); + // Set the Vault's target float percentage to the custom target float percentage // or the default target float percentage if a custom percentage isn't set. - vault.setTargetFloatPercent( - customTargetFloatPercent == 0 ? defaultTargetFloatPercent : customTargetFloatPercent - ); + vault.setTargetFloatPercent(newTargetFloatPercent); } } diff --git a/src/modules/VaultCreationModule.sol b/src/modules/VaultCreationModule.sol deleted file mode 100644 index 65571c9..0000000 --- a/src/modules/VaultCreationModule.sol +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -pragma solidity 0.8.9; - -import {ERC20} from "solmate/tokens/ERC20.sol"; -import {Auth, Authority} from "solmate/auth/Auth.sol"; - -import {Vault} from "../Vault.sol"; -import {VaultFactory} from "../VaultFactory.sol"; - -import {VaultConfigurationModule} from "./VaultConfigurationModule.sol"; - -/// @title Rari Vault Creation Module -/// @author Transmissions11 and JetJadeja -/// @notice Module for creating and configuring new Vaults. -contract VaultCreationModule is Auth { - /// @notice The Vault factory instance to deploy with. - VaultFactory public immutable FACTORY; - - /// @notice Vault configuration module instance to configure with. - VaultConfigurationModule public configModule; - - /// @notice Creates a Vault creation module. - /// @param _FACTORY The Vault factory instance the module should deploy with. - /// @param _configModule The Vault configuration module the module should configure with. - /// @param _owner The owner of the module. - /// @param _authority The authority of the module. - constructor( - VaultFactory _FACTORY, - VaultConfigurationModule _configModule, - address _owner, - Authority _authority - ) Auth(_owner, _authority) { - FACTORY = _FACTORY; - - configModule = _configModule; - } - - /// @notice Sets a new Vault configuration module. - /// @param newConfigModule The Vault configuration module to set. - function setConfigModule(VaultConfigurationModule newConfigModule) external requiresAuth { - configModule = newConfigModule; - } - - /// @notice Creates and properly configures a new Vault which supports a specific underlying token. - /// @dev This will revert if a Vault that accepts the same underlying token has already been deployed. - /// @param underlying The ERC20 token that the Vault should accept. - /// @return vault The newly deployed Vault contract which accepts the provided underlying token. - function createVault(ERC20 underlying) external returns (Vault vault) { - // Deploy a new Vault with the underlying token. - vault = FACTORY.deployVault(underlying); - - // Set all configuration parameters. - configModule.syncFeePercent(vault); - configModule.syncHarvestDelay(vault); - configModule.syncHarvestWindow(vault); - configModule.syncTargetFloatPercent(vault); - - // Open the Vault up for deposits. - vault.initialize(); - } -} diff --git a/src/modules/VaultETHWrapperModule.sol b/src/modules/VaultETHWrapperModule.sol deleted file mode 100644 index c60ca30..0000000 --- a/src/modules/VaultETHWrapperModule.sol +++ /dev/null @@ -1,108 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -pragma solidity 0.8.9; - -import {WETH} from "solmate/tokens/WETH.sol"; -import {ERC20} from "solmate/tokens/ERC20.sol"; -import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; -import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; - -import {Vault} from "../Vault.sol"; - -/// @title Rari Vault ETH Wrapper Module -/// @author Transmissions11 and JetJadeja -/// @notice Module for using ETH with a WETH Vault. -contract VaultETHWrapperModule { - using SafeTransferLib for ERC20; - using SafeTransferLib for address; - using FixedPointMathLib for uint256; - - /// @notice Deposit ETH into a WETH compatible Vault. - /// @param vault The WETH compatible Vault to deposit into. - /// @dev The caller must attach the amount they want to deposit as msg.value. - function depositETHIntoVault(Vault vault) external payable { - // Ensure the Vault's underlying token is WETH compatible. - require(vault.underlyingIsWETH(), "UNDERLYING_NOT_WETH"); - - // Get the Vault's underlying as WETH. - WETH weth = WETH(payable(address(vault.UNDERLYING()))); - - // Wrap the ETH into WETH. - weth.deposit{value: msg.value}(); - - // Approve the WETH to the Vault. - weth.approve(address(vault), msg.value); - - // Deposit the WETH into the Vault. - vault.deposit(msg.value); - - // Get the Vault's rvToken. - ERC20 rvToken = ERC20(vault); - - // Transfer the newly minted rvTokens back to the user. - rvToken.transfer(msg.sender, rvToken.balanceOf(address(this))); - } - - /// @notice Withdraw ETH from a WETH compatible Vault. - /// @param vault The WETH compatible Vault to withdraw from. - /// @param underlyingAmount The amount of ETH to withdraw from the Vault. - /// @dev The caller must approve the equivalent amount of rvTokens to the module. - function withdrawETHFromVault(Vault vault, uint256 underlyingAmount) external { - // Ensure the Vault's underlying token is WETH compatible. - require(vault.underlyingIsWETH(), "UNDERLYING_NOT_WETH"); - - // Compute the amount of rvTokens equivalent to the underlying amount. - // We know the Vault's base unit is 1e18 as it's required if underlyingIsWETH returns true. - uint256 rvTokenAmount = underlyingAmount.fdiv(vault.exchangeRate(), 1e18); - - // Get the Vault's rvToken. - ERC20 rvToken = ERC20(vault); - - // Transfer in the equivalent amount of rvTokens from the caller. - rvToken.safeTransferFrom(msg.sender, address(this), rvTokenAmount); - - // Withdraw from the Vault. - vault.withdraw(underlyingAmount); - - // Get the Vault's underlying as WETH. - WETH weth = WETH(payable(address(vault.UNDERLYING()))); - - // Convert the WETH into ETH. - weth.withdraw(underlyingAmount); - - // Transfer the unwrapped ETH to the caller. - msg.sender.safeTransferETH(underlyingAmount); - } - - /// @notice Redeem ETH from a WETH compatible Vault. - /// @param vault The WETH compatible Vault to redeem from. - /// @param rvTokenAmount The amount of rvTokens to withdraw from the Vault. - /// @dev The caller must approve the provided amount of rvTokens to the module. - function redeemETHFromVault(Vault vault, uint256 rvTokenAmount) external { - // Ensure the Vault accepts WETH. - require(vault.underlyingIsWETH(), "UNDERLYING_NOT_WETH"); - - // Get the Vault's rvToken. - ERC20 rvToken = ERC20(vault); - - // Transfer in the rvTokens from the caller. - rvToken.safeTransferFrom(msg.sender, address(this), rvTokenAmount); - - // Redeem the rvTokens. - vault.redeem(rvTokenAmount); - - // Get the Vault's underlying as WETH. - WETH weth = WETH(payable(address(vault.UNDERLYING()))); - - // Get how much WETH we redeemed. - uint256 withdrawnWETH = weth.balanceOf(address(this)); - - // Convert the WETH into ETH. - weth.withdraw(withdrawnWETH); - - // Transfer the unwrapped ETH to the caller. - msg.sender.safeTransferETH(withdrawnWETH); - } - - /// @dev Required for the module to receive unwrapped ETH. - receive() external payable {} -} diff --git a/src/modules/VaultInitializationModule.sol b/src/modules/VaultInitializationModule.sol new file mode 100644 index 0000000..d9357b8 --- /dev/null +++ b/src/modules/VaultInitializationModule.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.10; + +import {ERC20} from "solmate/tokens/ERC20.sol"; +import {Auth, Authority} from "solmate/auth/Auth.sol"; + +import {Vault} from "../Vault.sol"; +import {VaultFactory} from "../VaultFactory.sol"; + +import {VaultConfigurationModule} from "./VaultConfigurationModule.sol"; + +/// @title Rari Vault Initialization Module +/// @author Transmissions11 and JetJadeja +/// @notice Module for initializing newly created Vaults. +contract VaultInitializationModule is Auth { + /// @notice Vault configuration module used to configure Vaults before initialization. + VaultConfigurationModule public configModule; + + /// @notice Creates a Vault initialization module. + /// @param _configModule The Vault configuration module the + /// module will use to configure Vaults before initialization. + /// @param _owner The owner of the module. + /// @param _authority The Authority of the module. + constructor( + VaultConfigurationModule _configModule, + address _owner, + Authority _authority + ) Auth(_owner, _authority) { + configModule = _configModule; + } + + /// @notice Emitted when the config module is updated. + /// @param newConfigModule The new configuration module. + event ConfigModuleUpdated(VaultConfigurationModule newConfigModule); + + /// @notice Sets a new Vault configuration module. + /// @param newConfigModule The Vault configuration module to set. + function setConfigModule(VaultConfigurationModule newConfigModule) external requiresAuth { + // Update the config module. + configModule = newConfigModule; + + emit ConfigModuleUpdated(newConfigModule); + } + + /// @notice Properly configures and initializes a newly deployed Vault. + /// @dev This will revert if the Vault has already been initialized. + /// @param vault The Vault to configure and initialize. + function initializeVault(Vault vault) external { + // Configure all key parameters. + configModule.syncFeePercent(vault); + configModule.syncHarvestDelay(vault); + configModule.syncHarvestWindow(vault); + configModule.syncTargetFloatPercent(vault); + + // Open the Vault up for deposits. + vault.initialize(); + } +} diff --git a/src/modules/VaultRouterModule.sol b/src/modules/VaultRouterModule.sol new file mode 100644 index 0000000..df283a7 --- /dev/null +++ b/src/modules/VaultRouterModule.sol @@ -0,0 +1,304 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.10; + +import {WETH} from "solmate/tokens/WETH.sol"; +import {ERC20} from "solmate/tokens/ERC20.sol"; +import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; +import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; + +import {AllowedPermit} from "../interfaces/AllowedPermit.sol"; + +import {Vault} from "../Vault.sol"; + +/// @title Rari Vault Router Module +/// @author Transmissions11 and JetJadeja +/// @notice Module that enables depositing ETH into WETH compatible Vaults +/// and approval-free deposits into Vaults with permit compatible underlying. +contract VaultRouterModule { + using SafeTransferLib for ERC20; + using SafeTransferLib for address; + using FixedPointMathLib for uint256; + + /*/////////////////////////////////////////////////////////////// + DEPOSIT LOGIC + //////////////////////////////////////////////////////////////*/ + + /// @notice Deposit ETH into a WETH compatible Vault. + /// @param vault The WETH compatible Vault to deposit into. + function depositETHIntoVault(Vault vault) external payable { + // Ensure the Vault's underlying is stored as WETH compatible. + require(vault.underlyingIsWETH(), "UNDERLYING_NOT_WETH"); + + // Get the Vault's underlying as WETH. + WETH weth = WETH(payable(address(vault.UNDERLYING()))); + + // Wrap the ETH into WETH. + weth.deposit{value: msg.value}(); + + // Deposit and transfer the minted rvTokens back to the caller. + depositIntoVaultForCaller(vault, weth, msg.value); + } + + /// @notice Deposits into a Vault, transferring in its underlying token from the caller via permit. + /// @param vault The Vault to deposit into. + /// @param underlyingAmount The amount of underlying tokens to deposit into the Vault. + /// @param deadline A timestamp, the block's timestamp must be less than or equal to this timestamp. + /// @param v Must produce valid secp256k1 signature from the caller along with r and s. + /// @param r Must produce valid secp256k1 signature from the caller along with v and s. + /// @param s Must produce valid secp256k1 signature from the caller along with r and v. + /// @dev Use depositIntoVaultWithAllowedPermit for tokens using DAI's non-standard permit interface. + function depositIntoVaultWithPermit( + Vault vault, + uint256 underlyingAmount, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external { + // Get the Vault's underlying token. + ERC20 underlying = vault.UNDERLYING(); + + // Transfer in the provided amount of underlying tokens from the caller via permit. + permitAndTransferFromCaller(underlying, underlyingAmount, deadline, v, r, s); + + // Deposit and transfer the minted rvTokens back to the caller. + depositIntoVaultForCaller(vault, underlying, underlyingAmount); + } + + /// @notice Deposits into a Vault, transferring in its underlying token from the caller via allowed permit. + /// @param vault The Vault to deposit into. + /// @param underlyingAmount The amount of underlying tokens to deposit into the Vault. + /// @param nonce The callers's nonce, increases at each call to permit. + /// @param expiry The timestamp at which the permit is no longer valid. + /// @param v Must produce valid secp256k1 signature from the caller along with r and s. + /// @param r Must produce valid secp256k1 signature from the caller along with v and s. + /// @param s Must produce valid secp256k1 signature from the caller along with r and v. + /// @dev Alternative to depositIntoVaultWithPermit for tokens using DAI's non-standard permit interface. + function depositIntoVaultWithAllowedPermit( + Vault vault, + uint256 underlyingAmount, + uint256 nonce, + uint256 expiry, + uint8 v, + bytes32 r, + bytes32 s + ) external { + // Get the Vault's underlying token. + ERC20 underlying = vault.UNDERLYING(); + + // Transfer in the provided amount of underlying tokens from the caller via allowed permit. + allowedPermitAndTransferFromCaller(underlying, underlyingAmount, nonce, expiry, v, r, s); + + // Deposit and transfer the minted rvTokens back to the caller. + depositIntoVaultForCaller(vault, underlying, underlyingAmount); + } + + /*/////////////////////////////////////////////////////////////// + WITHDRAWAL LOGIC + //////////////////////////////////////////////////////////////*/ + + /// @notice Withdraw ETH from a WETH compatible Vault. + /// @param vault The WETH compatible Vault to withdraw from. + /// @param underlyingAmount The amount of ETH to withdraw from the Vault. + function withdrawETHFromVault(Vault vault, uint256 underlyingAmount) external { + // Ensure the Vault's underlying is stored as WETH compatible. + require(vault.underlyingIsWETH(), "UNDERLYING_NOT_WETH"); + + // Compute the amount of rvTokens equivalent to the underlying amount. + // We know the Vault's base unit is 1e18 as it's required for underlyingIsWETH to be true. + uint256 rvTokenAmount = underlyingAmount.divWadDown(vault.convertToAssets(10**vault.decimals())); + + // Transfer in the equivalent amount of rvTokens from the caller. + ERC20(vault).safeTransferFrom(msg.sender, address(this), rvTokenAmount); + + // Withdraw from the Vault. + vault.withdraw(underlyingAmount, address(this), address(this)); + + // Unwrap the withdrawn amount of WETH and transfer it to the caller. + unwrapAndTransfer(WETH(payable(address(vault.UNDERLYING()))), underlyingAmount); + } + + /// @notice Withdraw ETH from a WETH compatible Vault. + /// @param vault The WETH compatible Vault to withdraw from. + /// @param underlyingAmount The amount of ETH to withdraw from the Vault. + /// @param deadline A timestamp, the block's timestamp must be less than or equal to this timestamp. + /// @param v Must produce valid secp256k1 signature from the caller along with r and s. + /// @param r Must produce valid secp256k1 signature from the caller along with v and s. + /// @param s Must produce valid secp256k1 signature from the caller along with r and v. + function withdrawETHFromVaultWithPermit( + Vault vault, + uint256 underlyingAmount, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external { + // Ensure the Vault's underlying is stored as WETH compatible. + require(vault.underlyingIsWETH(), "UNDERLYING_NOT_WETH"); + + // Compute the amount of rvTokens equivalent to the underlying amount. + // We know the Vault's base unit is 1e18 as it's required for underlyingIsWETH to be true. + uint256 rvTokenAmount = underlyingAmount.divWadDown(vault.convertToAssets(10**vault.decimals())); + + // Transfer in the equivalent amount of rvTokens from the caller via permit. + permitAndTransferFromCaller(vault, rvTokenAmount, deadline, v, r, s); + + // Withdraw from the Vault. + vault.withdraw(underlyingAmount, address(this), address(this)); + + // Unwrap the withdrawn amount of WETH and transfer it to the caller. + unwrapAndTransfer(WETH(payable(address(vault.UNDERLYING()))), underlyingAmount); + } + + /*/////////////////////////////////////////////////////////////// + REDEEM LOGIC + //////////////////////////////////////////////////////////////*/ + + /// @notice Redeem ETH from a WETH compatible Vault. + /// @param vault The WETH compatible Vault to redeem from. + /// @param rvTokenAmount The amount of rvTokens to withdraw from the Vault. + function redeemETHFromVault(Vault vault, uint256 rvTokenAmount) external { + // Ensure the Vault's underlying is stored as WETH compatible. + require(vault.underlyingIsWETH(), "UNDERLYING_NOT_WETH"); + + // Transfer in the provided amount of rvTokens from the caller. + ERC20(vault).safeTransferFrom(msg.sender, address(this), rvTokenAmount); + + // Redeem the rvTokens. + vault.redeem(rvTokenAmount, address(this), address(this)); + + // Get the Vault's underlying as WETH. + WETH weth = WETH(payable(address(vault.UNDERLYING()))); + + // Unwrap all our WETH and transfer it to the caller. + unwrapAndTransfer(weth, weth.balanceOf(address(this))); + } + + /// @notice Redeem ETH from a WETH compatible Vault. + /// @param vault The WETH compatible Vault to redeem from. + /// @param rvTokenAmount The amount of rvTokens to withdraw from the Vault. + /// @param deadline A timestamp, the block's timestamp must be less than or equal to this timestamp. + /// @param v Must produce valid secp256k1 signature from the caller along with r and s. + /// @param r Must produce valid secp256k1 signature from the caller along with v and s. + /// @param s Must produce valid secp256k1 signature from the caller along with r and v. + function redeemETHFromVaultWithPermit( + Vault vault, + uint256 rvTokenAmount, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external { + // Ensure the Vault's underlying is stored as WETH compatible. + require(vault.underlyingIsWETH(), "UNDERLYING_NOT_WETH"); + + // Transfer in the provided amount of rvTokens from the caller via permit. + permitAndTransferFromCaller(vault, rvTokenAmount, deadline, v, r, s); + + // Redeem the rvTokens. + vault.redeem(rvTokenAmount, address(this), address(this)); + + // Get the Vault's underlying as WETH. + WETH weth = WETH(payable(address(vault.UNDERLYING()))); + + // Unwrap all our WETH and transfer it to the caller. + unwrapAndTransfer(weth, weth.balanceOf(address(this))); + } + + /*/////////////////////////////////////////////////////////////// + WETH UNWRAPPING LOGIC + //////////////////////////////////////////////////////////////*/ + + /// @dev Unwraps the provided amount of WETH and transfers it to the caller. + /// @param weth The WETH contract to withdraw the amount from. + /// @param amount The amount of WETH to unwrap into ETH and transfer. + function unwrapAndTransfer(WETH weth, uint256 amount) internal { + // Convert the WETH into ETH. + weth.withdraw(amount); + + // Transfer the unwrapped ETH to the caller. + msg.sender.safeTransferETH(amount); + } + + /*/////////////////////////////////////////////////////////////// + VAULT DEPOSIT LOGIC + //////////////////////////////////////////////////////////////*/ + + /// @dev Approves tokens, deposits them into a Vault + /// and transfers the minted rvTokens back to the caller. + /// @param vault The Vault to deposit into. + /// @param underlying The underlying token the Vault accepts. + /// @param amount The minimum amount that must be approved. + function depositIntoVaultForCaller( + Vault vault, + ERC20 underlying, + uint256 amount + ) internal { + // Approve the underlying tokens to the Vault. + underlying.safeApprove(address(vault), amount); + + // Deposit the underlying tokens into the Vault. + vault.deposit(amount, address(this)); + + // Transfer the newly minted rvTokens back to the caller. + ERC20(vault).safeTransfer(msg.sender, vault.balanceOf(address(this))); + } + + /*/////////////////////////////////////////////////////////////// + PERMIT LOGIC + //////////////////////////////////////////////////////////////*/ + + /// @dev Permits tokens from the caller and transfers them into the module. + /// @param token The token to permit and transfer in. + /// @param amount The amount of tokens to permit and transfer in. + /// @param deadline A timestamp, the block's timestamp must be less than or equal to this timestamp. + /// @param v Must produce valid secp256k1 signature from the caller along with r and s. + /// @param r Must produce valid secp256k1 signature from the caller along with v and s. + /// @param s Must produce valid secp256k1 signature from the caller along with r and v. + function permitAndTransferFromCaller( + ERC20 token, + uint256 amount, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) internal { + // Approve the tokens from the caller to the module via permit. + token.permit(msg.sender, address(this), amount, deadline, v, r, s); + + // Transfer the tokens from the caller to the module. + token.safeTransferFrom(msg.sender, address(this), amount); + } + + /// @dev Max permits tokens from the caller and transfers them into the module. + /// @param token The token to permit and transfer in. + /// @param amount The amount of tokens to permit and transfer in. + /// @param nonce The callers's nonce, increases at each call to permit. + /// @param expiry The timestamp at which the permit is no longer valid. + /// @param v Must produce valid secp256k1 signature from the caller along with r and s. + /// @param r Must produce valid secp256k1 signature from the caller along with v and s. + /// @param s Must produce valid secp256k1 signature from the caller along with r and v. + /// @dev Alternative to permitAndTransferFromCaller for tokens using DAI's non-standard permit interface. + function allowedPermitAndTransferFromCaller( + ERC20 token, + uint256 amount, + uint256 nonce, + uint256 expiry, + uint8 v, + bytes32 r, + bytes32 s + ) internal { + // Approve the tokens from the caller to the module via DAI's non-standard permit. + AllowedPermit(address(token)).permit(msg.sender, address(this), nonce, expiry, true, v, r, s); + + // Transfer the tokens from the caller to the module. + token.safeTransferFrom(msg.sender, address(this), amount); + } + + /*/////////////////////////////////////////////////////////////// + RECIEVE ETHER LOGIC + //////////////////////////////////////////////////////////////*/ + + /// @dev Required for the module to receive unwrapped ETH. + receive() external payable {} +} diff --git a/src/test/Integration.t.sol b/src/test/Integration.t.sol new file mode 100644 index 0000000..5f58e09 --- /dev/null +++ b/src/test/Integration.t.sol @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.10; + +import {Authority} from "solmate/auth/Auth.sol"; +import {DSTestPlus} from "solmate/test/utils/DSTestPlus.sol"; +import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; +import {MultiRolesAuthority} from "solmate/auth/authorities/MultiRolesAuthority.sol"; + +import {MockERC20Strategy} from "./mocks/MockERC20Strategy.sol"; + +import {VaultInitializationModule} from "../modules/VaultInitializationModule.sol"; +import {VaultConfigurationModule} from "../modules/VaultConfigurationModule.sol"; + +import {Strategy} from "../interfaces/Strategy.sol"; + +import {Vault} from "../Vault.sol"; +import {VaultFactory} from "../VaultFactory.sol"; + +contract IntegrationTest is DSTestPlus { + VaultFactory vaultFactory; + + MultiRolesAuthority multiRolesAuthority; + + VaultConfigurationModule vaultConfigurationModule; + + VaultInitializationModule vaultInitializationModule; + + MockERC20 underlying; + + MockERC20Strategy strategy1; + MockERC20Strategy strategy2; + + function setUp() public { + underlying = new MockERC20("Mock Token", "TKN", 18); + + multiRolesAuthority = new MultiRolesAuthority(address(this), Authority(address(0))); + + vaultFactory = new VaultFactory(address(this), multiRolesAuthority); + + vaultConfigurationModule = new VaultConfigurationModule(address(this), Authority(address(0))); + + vaultInitializationModule = new VaultInitializationModule( + vaultConfigurationModule, + address(this), + Authority(address(0)) + ); + + strategy1 = new MockERC20Strategy(underlying); + strategy2 = new MockERC20Strategy(underlying); + } + + function testIntegration() public { + multiRolesAuthority.setUserRole(address(vaultConfigurationModule), 0, true); + multiRolesAuthority.setRoleCapability(0, Vault.setFeePercent.selector, true); + multiRolesAuthority.setRoleCapability(0, Vault.setHarvestDelay.selector, true); + multiRolesAuthority.setRoleCapability(0, Vault.setHarvestWindow.selector, true); + multiRolesAuthority.setRoleCapability(0, Vault.setTargetFloatPercent.selector, true); + + multiRolesAuthority.setUserRole(address(vaultInitializationModule), 1, true); + multiRolesAuthority.setRoleCapability(1, Vault.initialize.selector, true); + + vaultConfigurationModule.setDefaultFeePercent(0.1e18); + vaultConfigurationModule.setDefaultHarvestDelay(6 hours); + vaultConfigurationModule.setDefaultHarvestWindow(5 minutes); + vaultConfigurationModule.setDefaultTargetFloatPercent(0.01e18); + + Vault vault = vaultFactory.deployVault(underlying); + vaultInitializationModule.initializeVault(vault); + + underlying.mint(address(this), 1.5e18); + + underlying.approve(address(vault), 1e18); + vault.deposit(1e18, address(this)); + + vault.trustStrategy(strategy1); + vault.depositIntoStrategy(strategy1, 0.5e18); + vault.pushToWithdrawalStack(strategy1); + + vault.trustStrategy(strategy2); + vault.depositIntoStrategy(strategy2, 0.5e18); + vault.pushToWithdrawalStack(strategy2); + + vaultConfigurationModule.setDefaultFeePercent(0.2e18); + assertEq(vault.feePercent(), 0.1e18); + + vaultConfigurationModule.syncFeePercent(vault); + assertEq(vault.feePercent(), 0.2e18); + + underlying.transfer(address(strategy1), 0.25e18); + + Strategy[] memory strategiesToHarvest = new Strategy[](2); + strategiesToHarvest[0] = strategy1; + strategiesToHarvest[1] = strategy2; + + underlying.transfer(address(strategy2), 0.25e18); + vault.harvest(strategiesToHarvest); + + hevm.warp(block.timestamp + vault.harvestDelay()); + + vault.withdraw(1363636363636363636, address(this), address(this)); + assertEq(vault.balanceOf(address(this)), 0); + } +} diff --git a/src/test/Vault.t.sol b/src/test/Vault.t.sol index f9fe8c2..46cf826 100644 --- a/src/test/Vault.t.sol +++ b/src/test/Vault.t.sol @@ -1,27 +1,32 @@ // SPDX-License-Identifier: AGPL-3.0-only -pragma solidity 0.8.9; +pragma solidity 0.8.10; +import {WETH} from "solmate/tokens/WETH.sol"; +import {Authority} from "solmate/auth/Auth.sol"; +import {DSTestPlus} from "solmate/test/utils/DSTestPlus.sol"; import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; -import {DSTestPlus} from "./utils/DSTestPlus.sol"; - -import {MockStrategy} from "./mocks/MockStrategy.sol"; +import {MockETHStrategy} from "./mocks/MockETHStrategy.sol"; +import {MockERC20Strategy} from "./mocks/MockERC20Strategy.sol"; import {Strategy} from "../interfaces/Strategy.sol"; import {Vault} from "../Vault.sol"; import {VaultFactory} from "../VaultFactory.sol"; +import "forge-std/console.sol"; + contract VaultsTest is DSTestPlus { Vault vault; MockERC20 underlying; - MockStrategy strategy1; - MockStrategy strategy2; + MockERC20Strategy strategy1; + MockERC20Strategy strategy2; function setUp() public { underlying = new MockERC20("Mock Token", "TKN", 18); - vault = new VaultFactory().deployVault(underlying); + + vault = new VaultFactory(address(this), Authority(address(0))).deployVault(underlying); vault.setFeePercent(0.1e18); vault.setHarvestDelay(6 hours); @@ -30,12 +35,12 @@ contract VaultsTest is DSTestPlus { vault.initialize(); - strategy1 = new MockStrategy(underlying); - strategy2 = new MockStrategy(underlying); + strategy1 = new MockERC20Strategy(underlying); + strategy2 = new MockERC20Strategy(underlying); } /*/////////////////////////////////////////////////////////////// - BASIC DEPOSIT/WITHDRAWAL TESTS + DEPOSIT/WITHDRAWAL TESTS //////////////////////////////////////////////////////////////*/ function testAtomicDepositWithdraw() public { @@ -44,24 +49,53 @@ contract VaultsTest is DSTestPlus { uint256 preDepositBal = underlying.balanceOf(address(this)); - vault.deposit(1e18); + vault.deposit(1e18, address(this)); - assertEq(vault.exchangeRate(), 1e18); + assertEq(vault.convertToAssets(10**vault.decimals()), 1e18); assertEq(vault.totalStrategyHoldings(), 0); - assertEq(vault.totalHoldings(), 1e18); + assertEq(vault.totalAssets(), 1e18); assertEq(vault.totalFloat(), 1e18); assertEq(vault.balanceOf(address(this)), 1e18); - assertEq(vault.balanceOfUnderlying(address(this)), 1e18); + assertEq(vault.convertToAssets(vault.balanceOf(address(this))), 1e18); assertEq(underlying.balanceOf(address(this)), preDepositBal - 1e18); - vault.withdraw(1e18); + vault.withdraw(1e18, address(this), address(this)); - assertEq(vault.exchangeRate(), 1e18); + assertEq(vault.convertToAssets(10**vault.decimals()), 1e18); assertEq(vault.totalStrategyHoldings(), 0); - assertEq(vault.totalHoldings(), 0); + assertEq(vault.totalAssets(), 0); assertEq(vault.totalFloat(), 0); assertEq(vault.balanceOf(address(this)), 0); - assertEq(vault.balanceOfUnderlying(address(this)), 0); + assertEq(vault.convertToAssets(vault.balanceOf(address(this))), 0); + assertEq(underlying.balanceOf(address(this)), preDepositBal); + } + + function testDepositWithdraw(uint256 amount) public { + amount = bound(amount, 1e5, 1e27); + + underlying.mint(address(this), amount); + underlying.approve(address(vault), amount); + + uint256 preDepositBal = underlying.balanceOf(address(this)); + + vault.deposit(amount, address(this)); + + assertEq(vault.convertToAssets(10**vault.decimals()), 1e18); + assertEq(vault.totalStrategyHoldings(), 0); + assertEq(vault.totalAssets(), amount); + assertEq(vault.totalFloat(), amount); + assertEq(vault.balanceOf(address(this)), amount); + assertEq(vault.convertToAssets(vault.balanceOf(address(this))), amount); + assertEq(underlying.balanceOf(address(this)), preDepositBal - amount); + + vault.withdraw(amount, address(this), address(this)); + + assertEq(vault.convertToAssets(10**vault.decimals()), 1e18); + assertEq(vault.totalStrategyHoldings(), 0); + assertEq(vault.totalAssets(), 0); + assertEq(vault.totalFloat(), 0); + assertEq(vault.balanceOf(address(this)), 0); + assertEq(vault.convertToAssets(vault.balanceOf(address(this))), 0); assertEq(underlying.balanceOf(address(this)), preDepositBal); } @@ -71,524 +105,888 @@ contract VaultsTest is DSTestPlus { uint256 preDepositBal = underlying.balanceOf(address(this)); - vault.deposit(1e18); + vault.deposit(1e18, address(this)); - assertEq(vault.exchangeRate(), 1e18); + assertEq(vault.convertToAssets(10**vault.decimals()), 1e18); assertEq(vault.totalStrategyHoldings(), 0); - assertEq(vault.totalHoldings(), 1e18); + assertEq(vault.totalAssets(), 1e18); assertEq(vault.totalFloat(), 1e18); assertEq(vault.balanceOf(address(this)), 1e18); - assertEq(vault.balanceOfUnderlying(address(this)), 1e18); + assertEq(vault.convertToAssets(vault.balanceOf(address(this))), 1e18); assertEq(underlying.balanceOf(address(this)), preDepositBal - 1e18); - vault.redeem(1e18); + vault.redeem(1e18, address(this), address(this)); - assertEq(vault.exchangeRate(), 1e18); + assertEq(vault.convertToAssets(10**vault.decimals()), 1e18); assertEq(vault.totalStrategyHoldings(), 0); - assertEq(vault.totalHoldings(), 0); + assertEq(vault.totalAssets(), 0); assertEq(vault.totalFloat(), 0); assertEq(vault.balanceOf(address(this)), 0); - assertEq(vault.balanceOfUnderlying(address(this)), 0); + assertEq(vault.convertToAssets(vault.balanceOf(address(this))), 0); assertEq(underlying.balanceOf(address(this)), preDepositBal); } - /*/////////////////////////////////////////////////////////////// - DEPOSIT/WITHDRAWAL SANITY CHECK TESTS - //////////////////////////////////////////////////////////////*/ + function testDepositRedeem(uint256 amount) public { + amount = bound(amount, 1e5, 1e27); - function testFailDepositWithNotEnoughApproval() public { - underlying.mint(address(this), 0.5e18); - underlying.approve(address(vault), 0.5e18); + underlying.mint(address(this), amount); + underlying.approve(address(vault), amount); - vault.deposit(1e18); - } + uint256 preDepositBal = underlying.balanceOf(address(this)); - function testFailWithdrawWithNotEnoughBalance() public { - underlying.mint(address(this), 0.5e18); - underlying.approve(address(vault), 0.5e18); + vault.deposit(amount, address(this)); + + assertEq(vault.convertToAssets(10**vault.decimals()), 1e18); + assertEq(vault.totalStrategyHoldings(), 0); + assertEq(vault.totalAssets(), amount); + assertEq(vault.totalFloat(), amount); + assertEq(vault.balanceOf(address(this)), amount); + assertEq(vault.convertToAssets(vault.balanceOf(address(this))), amount); + assertEq(underlying.balanceOf(address(this)), preDepositBal - amount); - vault.deposit(0.5e18); + vault.redeem(amount, address(this), address(this)); - vault.withdraw(1e18); + assertEq(vault.convertToAssets(10**vault.decimals()), 1e18); + assertEq(vault.totalStrategyHoldings(), 0); + assertEq(vault.totalAssets(), 0); + assertEq(vault.totalFloat(), 0); + assertEq(vault.balanceOf(address(this)), 0); + assertEq(vault.convertToAssets(vault.balanceOf(address(this))), 0); + assertEq(underlying.balanceOf(address(this)), preDepositBal); } - function testFailRedeemWithNotEnoughBalance() public { - underlying.mint(address(this), 0.5e18); - underlying.approve(address(vault), 0.5e18); + /*/////////////////////////////////////////////////////////////// + DEPOSIT/WITHDRAWAL SANITY CHECK TESTS + //////////////////////////////////////////////////////////////*/ - vault.deposit(0.5e18); + function testFailDepositWithNotEnoughApproval(uint256 amount) public { + underlying.mint(address(this), amount / 2); + underlying.approve(address(vault), amount / 2); - vault.redeem(1e18); + vault.deposit(amount, address(this)); } - function testFailRedeemWithNoBalance() public { - vault.redeem(1e18); - } + function testFailWithdrawWithNotEnoughBalance(uint256 amount) public { + underlying.mint(address(this), amount / 2); + underlying.approve(address(vault), amount / 2); - function testFailWithdrawWithNoBalance() public { - vault.withdraw(1e18); + vault.deposit(amount / 2, address(this)); + + vault.withdraw(amount, address(this), address(this)); } - function testFailDepositWithNoApproval() public { - vault.deposit(1e18); + function testFailRedeemWithNotEnoughBalance(uint256 amount) public { + underlying.mint(address(this), amount / 2); + underlying.approve(address(vault), amount / 2); + + vault.deposit(amount / 2, address(this)); + + vault.redeem(amount, address(this), address(this)); } - function testFailRedeemZero() public { - vault.redeem(0); + function testFailWithdrawWithNoBalance(uint256 amount) public { + if (amount == 0) amount = 1; + vault.withdraw(amount, address(this), address(this)); } - function testFailWithdrawZero() public { - vault.withdraw(0); + function testFailRedeemWithNoBalance(uint256 amount) public { + vault.redeem(amount, address(this), address(this)); } - function testFailDepositZero() public { - vault.deposit(0); + function testFailDepositWithNoApproval(uint256 amount) public { + vault.deposit(amount, address(this)); } /*/////////////////////////////////////////////////////////////// - BASIC STRATEGY DEPOSIT/WITHDRAWAL TESTS + STRATEGY DEPOSIT/WITHDRAWAL TESTS //////////////////////////////////////////////////////////////*/ - function testAtomicEnterExitSinglePool() public { - underlying.mint(address(this), 1e18); - underlying.approve(address(vault), 1e18); - vault.deposit(1e18); + function testAtomicEnterExitSinglePool(uint256 amount) public { + amount = bound(amount, 1e12, 1e27); - vault.trustStrategy(strategy1); + underlying.mint(address(this), amount); + underlying.approve(address(vault), amount); + vault.deposit(amount, address(this)); - vault.depositIntoStrategy(strategy1, 1e18); + vault.trustStrategy(strategy1); + vault.depositIntoStrategy(strategy1, amount); - assertEq(vault.exchangeRate(), 1e18); - assertEq(vault.totalStrategyHoldings(), 1e18); - assertEq(vault.totalHoldings(), 1e18); + assertEq(vault.convertToAssets(10**vault.decimals()), 1e18); + assertEq(vault.totalStrategyHoldings(), amount); + assertEq(vault.totalAssets(), amount); assertEq(vault.totalFloat(), 0); - assertEq(vault.balanceOf(address(this)), 1e18); - assertEq(vault.balanceOfUnderlying(address(this)), 1e18); - - vault.withdrawFromStrategy(strategy1, 0.5e18); - - assertEq(vault.exchangeRate(), 1e18); - assertEq(vault.totalStrategyHoldings(), 0.5e18); - assertEq(vault.totalHoldings(), 1e18); - assertEq(vault.totalFloat(), 0.5e18); - assertEq(vault.balanceOf(address(this)), 1e18); - assertEq(vault.balanceOfUnderlying(address(this)), 1e18); - - vault.withdrawFromStrategy(strategy1, 0.5e18); - - assertEq(vault.exchangeRate(), 1e18); - assertEq(vault.totalStrategyHoldings(), 0); - assertEq(vault.totalHoldings(), 1e18); - assertEq(vault.totalFloat(), 1e18); - assertEq(vault.balanceOf(address(this)), 1e18); - assertEq(vault.balanceOfUnderlying(address(this)), 1e18); + assertEq(vault.balanceOf(address(this)), amount); + assertEq(vault.convertToAssets(vault.balanceOf(address(this))), amount); + + vault.withdrawFromStrategy(strategy1, amount / 2); + + assertEq(vault.convertToAssets(10**vault.decimals()), 1e18); + assertEq(vault.totalAssets(), amount); + assertEq(vault.totalFloat(), amount / 2); + assertEq(vault.balanceOf(address(this)), amount); + assertEq(vault.convertToAssets(vault.balanceOf(address(this))), amount); + assertApproxEq(vault.totalStrategyHoldings(), amount / 2, 2); + + vault.withdrawFromStrategy(strategy1, amount / 2); + + assertEq(vault.convertToAssets(10**vault.decimals()), 1e18); + assertEq(vault.totalAssets(), amount); + assertEq(vault.balanceOf(address(this)), amount); + assertEq(vault.convertToAssets(vault.balanceOf(address(this))), amount); + assertApproxEq(vault.totalFloat(), amount, 2); // Approx + assertEq(vault.totalStrategyHoldings() / 10, 0); // Aprox } - function testAtomicEnterExitMultiPool() public { - underlying.mint(address(this), 1e18); - underlying.approve(address(vault), 1e18); - vault.deposit(1e18); + function testAtomicEnterExitMultiPool(uint256 amount) public { + amount = bound(amount, 1e5, 1e36); + + underlying.mint(address(this), amount); + underlying.approve(address(vault), amount); + vault.deposit(amount, address(this)); vault.trustStrategy(strategy1); + vault.depositIntoStrategy(strategy1, amount / 2); - vault.depositIntoStrategy(strategy1, 0.5e18); - - assertEq(vault.exchangeRate(), 1e18); - assertEq(vault.totalStrategyHoldings(), 0.5e18); - assertEq(vault.totalHoldings(), 1e18); - assertEq(vault.totalFloat(), 0.5e18); - assertEq(vault.balanceOf(address(this)), 1e18); - assertEq(vault.balanceOfUnderlying(address(this)), 1e18); + assertEq(vault.convertToAssets(10**vault.decimals()), 1e18); + assertEq(vault.totalStrategyHoldings(), amount / 2); + assertEq(vault.totalAssets(), amount); + assertEq(vault.balanceOf(address(this)), amount); + assertEq(vault.convertToAssets(vault.balanceOf(address(this))), amount); + assertApproxEq(vault.totalFloat(), amount / 2, 2); // Approx vault.trustStrategy(strategy2); + vault.depositIntoStrategy(strategy2, amount / 2); - vault.depositIntoStrategy(strategy2, 0.5e18); + assertEq(vault.convertToAssets(10**vault.decimals()), 1e18); + assertEq(vault.totalAssets(), amount); + assertEq(vault.balanceOf(address(this)), amount); + assertEq(vault.convertToAssets(vault.balanceOf(address(this))), amount); + assertApproxEq(vault.totalStrategyHoldings(), amount, 2); // Approx + assertLt(vault.totalFloat(), 2); - assertEq(vault.exchangeRate(), 1e18); - assertEq(vault.totalStrategyHoldings(), 1e18); - assertEq(vault.totalHoldings(), 1e18); - assertEq(vault.totalFloat(), 0); - assertEq(vault.balanceOf(address(this)), 1e18); - assertEq(vault.balanceOfUnderlying(address(this)), 1e18); + vault.withdrawFromStrategy(strategy1, amount / 2); - vault.withdrawFromStrategy(strategy1, 0.5e18); + assertEq(vault.convertToAssets(10**vault.decimals()), 1e18); + assertEq(vault.totalStrategyHoldings(), amount / 2); + assertEq(vault.totalAssets(), amount); + assertApproxEq(vault.totalFloat(), amount / 2, 2); // Approx + assertEq(vault.balanceOf(address(this)), amount); + assertEq(vault.convertToAssets(vault.balanceOf(address(this))), amount); - assertEq(vault.exchangeRate(), 1e18); - assertEq(vault.totalStrategyHoldings(), 0.5e18); - assertEq(vault.totalHoldings(), 1e18); - assertEq(vault.totalFloat(), 0.5e18); - assertEq(vault.balanceOf(address(this)), 1e18); - assertEq(vault.balanceOfUnderlying(address(this)), 1e18); + vault.withdrawFromStrategy(strategy2, amount / 2); - vault.withdrawFromStrategy(strategy2, 0.5e18); - - assertEq(vault.exchangeRate(), 1e18); + assertEq(vault.convertToAssets(10**vault.decimals()), 1e18); assertEq(vault.totalStrategyHoldings(), 0); - assertEq(vault.totalHoldings(), 1e18); - assertEq(vault.totalFloat(), 1e18); - assertEq(vault.balanceOf(address(this)), 1e18); - assertEq(vault.balanceOfUnderlying(address(this)), 1e18); + assertEq(vault.totalAssets(), amount); + assertEq(vault.totalFloat(), amount); + assertEq(vault.balanceOf(address(this)), amount); + assertEq(vault.convertToAssets(vault.balanceOf(address(this))), amount); } /*/////////////////////////////////////////////////////////////// STRATEGY DEPOSIT/WITHDRAWAL SANITY CHECK TESTS //////////////////////////////////////////////////////////////*/ - function testFailDepositIntoStrategyWithNotEnoughBalance() public { - underlying.mint(address(this), 0.5e18); - underlying.approve(address(vault), 0.5e18); + function testFailDepositIntoStrategyWithNotEnoughBalance(uint256 amount) public { + amount = bound(amount, 1e5, 1e36); + + underlying.mint(address(this), amount / 2); + underlying.approve(address(vault), amount / 2); - vault.deposit(0.5e18); + vault.deposit(amount / 2, address(this)); vault.trustStrategy(strategy1); - vault.depositIntoStrategy(strategy1, 1e18); + vault.depositIntoStrategy(strategy1, amount); } - function testFailWithdrawFromStrategyWithNotEnoughBalance() public { - underlying.mint(address(this), 0.5e18); - underlying.approve(address(vault), 0.5e18); + function testFailWithdrawFromStrategyWithNotEnoughBalance(uint256 amount) public { + amount = bound(amount, 1e5, 1e36); - vault.deposit(0.5e18); + underlying.mint(address(this), amount / 2); + underlying.approve(address(vault), amount / 2); + + vault.deposit(amount / 2, address(this)); vault.trustStrategy(strategy1); - vault.depositIntoStrategy(strategy1, 0.5e18); + vault.depositIntoStrategy(strategy1, amount / 2); - vault.withdrawFromStrategy(strategy1, 1e18); + vault.withdrawFromStrategy(strategy1, amount); } - function testFailWithdrawFromStrategyWithoutTrust() public { - underlying.mint(address(this), 1e18); - underlying.approve(address(vault), 1e18); + function testFailWithdrawFromStrategyWithoutTrust(uint256 amount) public { + amount = bound(amount, 1e5, 1e36); + + underlying.mint(address(this), amount); + underlying.approve(address(vault), amount); - vault.deposit(1e18); + vault.deposit(amount, address(this)); vault.trustStrategy(strategy1); - vault.depositIntoStrategy(strategy1, 1e18); + vault.depositIntoStrategy(strategy1, amount); vault.distrustStrategy(strategy1); - - vault.withdrawFromStrategy(strategy1, 1e18); + vault.withdrawFromStrategy(strategy1, amount); } - function testFailDepositIntoStrategyWithNoBalance() public { - vault.trustStrategy(strategy1); - - vault.depositIntoStrategy(strategy1, 1e18); - } + function testFailDepositIntoStrategyWithNoBalance(uint256 amount) public { + amount = bound(amount, 1e5, 1e36); - function testFailWithdrawFromStrategyWithNoBalance() public { vault.trustStrategy(strategy1); - - vault.withdrawFromStrategy(strategy1, 1e18); + vault.depositIntoStrategy(strategy1, amount); } - function testFailDepositIntoStrategyZero() public { - vault.trustStrategy(strategy1); + function testFailWithdrawFromStrategyWithNoBalance(uint256 amount) public { + amount = bound(amount, 1e5, 1e36); - vault.depositIntoStrategy(strategy1, 0); - } - - function testFailWithdrawFromStrategyZero() public { vault.trustStrategy(strategy1); - - vault.withdrawFromStrategy(strategy1, 0); + vault.withdrawFromStrategy(strategy1, 1e18); } /*/////////////////////////////////////////////////////////////// - BASIC HARVEST TESTS + HARVEST TESTS //////////////////////////////////////////////////////////////*/ - function testProfitableHarvest() public { - underlying.mint(address(this), 1.5e18); + function testProfitableHarvest(uint256 amount) public { + amount = bound(amount, 1e5, 1e36); + uint256 total = (1.5e18 * amount) / 1e18; - underlying.approve(address(vault), 1e18); - vault.deposit(1e18); + underlying.mint(address(this), total); + underlying.approve(address(vault), amount); + vault.deposit(amount, address(this)); vault.trustStrategy(strategy1); - vault.depositIntoStrategy(strategy1, 1e18); - vault.pushToWithdrawalQueue(strategy1); + vault.depositIntoStrategy(strategy1, amount); + vault.pushToWithdrawalStack(strategy1); - assertEq(vault.exchangeRate(), 1e18); - assertEq(vault.totalStrategyHoldings(), 1e18); + assertEq(vault.convertToAssets(10**vault.decimals()), 1e18); + assertEq(vault.totalStrategyHoldings(), amount); assertEq(vault.totalFloat(), 0); - assertEq(vault.totalHoldings(), 1e18); - assertEq(vault.balanceOf(address(this)), 1e18); - assertEq(vault.balanceOfUnderlying(address(this)), 1e18); - assertEq(vault.totalSupply(), 1e18); + assertEq(vault.totalAssets(), amount); + assertEq(vault.balanceOf(address(this)), amount); + assertEq(vault.convertToAssets(vault.balanceOf(address(this))), amount); + assertEq(vault.totalSupply(), amount); assertEq(vault.balanceOf(address(vault)), 0); - assertEq(vault.balanceOfUnderlying(address(vault)), 0); + assertEq(vault.convertToAssets(vault.balanceOf(address(vault))), 0); - underlying.transfer(address(strategy1), 0.5e18); + underlying.transfer(address(strategy1), amount / 2); - assertEq(vault.exchangeRate(), 1e18); - assertEq(vault.totalStrategyHoldings(), 1e18); + assertEq(vault.convertToAssets(10**vault.decimals()), 1e18); + assertEq(vault.totalStrategyHoldings(), amount); assertEq(vault.totalFloat(), 0); - assertEq(vault.totalHoldings(), 1e18); - assertEq(vault.balanceOf(address(this)), 1e18); - assertEq(vault.balanceOfUnderlying(address(this)), 1e18); - assertEq(vault.totalSupply(), 1e18); + assertEq(vault.totalAssets(), amount); + assertEq(vault.balanceOf(address(this)), amount); + assertEq(vault.convertToAssets(vault.balanceOf(address(this))), amount); + assertEq(vault.totalSupply(), amount); assertEq(vault.balanceOf(address(vault)), 0); - assertEq(vault.balanceOfUnderlying(address(vault)), 0); + assertEq(vault.convertToAssets(vault.balanceOf(address(vault))), 0); + assertEq(vault.lastHarvest(), 0); + assertEq(vault.lastHarvestWindowStart(), 0); + + Strategy[] memory strategiesToHarvest = new Strategy[](1); + strategiesToHarvest[0] = strategy1; - vault.harvest(strategy1); + vault.harvest(strategiesToHarvest); + uint256 startingTimestamp = block.timestamp; - assertEq(vault.exchangeRate(), 1e18); - assertEq(vault.totalStrategyHoldings(), 1.5e18); + assertEq(vault.lastHarvest(), startingTimestamp); + assertEq(vault.lastHarvestWindowStart(), startingTimestamp); + assertEq(vault.convertToAssets(10**vault.decimals()), 1e18); + assertApproxEq(vault.totalStrategyHoldings(), total, 1); assertEq(vault.totalFloat(), 0); - assertEq(vault.totalHoldings(), 1.05e18); - assertEq(vault.balanceOf(address(this)), 1e18); - assertEq(vault.balanceOfUnderlying(address(this)), 1e18); - assertEq(vault.totalSupply(), 1.05e18); - assertEq(vault.balanceOf(address(vault)), 0.05e18); - assertEq(vault.balanceOfUnderlying(address(vault)), 0.05e18); + assertEq(vault.totalAssets(), (1.05e18 * amount) / 1e18); + assertEq(vault.balanceOf(address(this)), amount); + assertEq(vault.convertToAssets(vault.balanceOf(address(this))), amount); + assertEq(vault.totalSupply(), (1.05e18 * amount) / 1e18); + assertEq(vault.balanceOf(address(vault)), (0.05e18 * amount) / 1e18); + assertEq(vault.convertToAssets(vault.balanceOf(address(vault))), (0.05e18 * amount) / 1e18); hevm.warp(block.timestamp + (vault.harvestDelay() / 2)); - assertEq(vault.exchangeRate(), 1214285714285714285); - assertEq(vault.totalStrategyHoldings(), 1.5e18); + assertEq(vault.totalStrategyHoldings(), total); assertEq(vault.totalFloat(), 0); - assertEq(vault.totalHoldings(), 1.275e18); - assertEq(vault.balanceOf(address(this)), 1e18); - assertEq(vault.balanceOfUnderlying(address(this)), 1214285714285714285); - assertEq(vault.totalSupply(), 1.05e18); - assertEq(vault.balanceOf(address(vault)), 0.05e18); - assertEq(vault.balanceOfUnderlying(address(vault)), 60714285714285714); + assertGt(vault.totalAssets(), amount); + assertLt(vault.totalAssets(), total); + assertEq(vault.balanceOf(address(this)), amount); + assertEq(vault.totalSupply(), (1.05e18 * amount) / 1e18); + assertEq(vault.balanceOf(address(vault)), (0.05e18 * amount) / 1e18); + + assertGt(vault.convertToAssets(vault.balanceOf(address(this))), amount); + assertLt(vault.convertToAssets(vault.balanceOf(address(this))), (1.25e18 * amount) / 1e18); + assertGt(vault.convertToAssets(10**vault.decimals()), 1e18); + assertLt(vault.convertToAssets(10**vault.decimals()), 1.25e18); hevm.warp(block.timestamp + vault.harvestDelay()); - assertEq(vault.exchangeRate(), 1428571428571428571); - assertEq(vault.totalStrategyHoldings(), 1.5e18); + assertEq(vault.totalStrategyHoldings(), total); assertEq(vault.totalFloat(), 0); - assertEq(vault.totalHoldings(), 1.5e18); - assertEq(vault.balanceOf(address(this)), 1e18); - assertEq(vault.balanceOfUnderlying(address(this)), 1428571428571428571); - assertEq(vault.totalSupply(), 1.05e18); - assertEq(vault.balanceOf(address(vault)), 0.05e18); - assertEq(vault.balanceOfUnderlying(address(vault)), 71428571428571428); - - vault.redeem(1e18); - - assertEq(underlying.balanceOf(address(this)), 1428571428571428571); - - assertEq(vault.exchangeRate(), 1428571428571428580); - assertEq(vault.totalStrategyHoldings(), 70714285714285715); - assertEq(vault.totalFloat(), 714285714285714); - assertEq(vault.totalHoldings(), 71428571428571429); + assertEq(vault.totalAssets(), total); + assertEq(vault.balanceOf(address(this)), amount); + assertEq(vault.totalSupply(), (1.05e18 * amount) / 1e18); + assertEq(vault.balanceOf(address(vault)), (0.05e18 * amount) / 1e18); + + assertGt(vault.convertToAssets(vault.balanceOf(address(this))), (1.4e18 * amount) / 1e18); + assertLt(vault.convertToAssets(vault.balanceOf(address(this))), (1.5e18 * amount) / 1e18); + assertGt(vault.convertToAssets(10**vault.decimals()), 1.4e18); + assertLt(vault.convertToAssets(10**vault.decimals()), 1.5e18); + + vault.redeem(amount, address(this), address(this)); + + assertGt(vault.convertToAssets(10**vault.decimals()), 1.4e18); + assertEq(vault.totalStrategyHoldings(), vault.totalAssets() - vault.totalFloat()); + assertGt(vault.totalFloat(), 0); + assertGt(vault.totalAssets(), 0); assertEq(vault.balanceOf(address(this)), 0); - assertEq(vault.balanceOfUnderlying(address(this)), 0); - assertEq(vault.totalSupply(), 0.05e18); - assertEq(vault.balanceOf(address(vault)), 0.05e18); - assertEq(vault.balanceOfUnderlying(address(vault)), 71428571428571429); + assertEq(vault.convertToAssets(vault.balanceOf(address(this))), 0); + assertEq(vault.totalSupply(), (0.05e18 * amount) / 1e18); + assertEq(vault.balanceOf(address(vault)), (0.05e18 * amount) / 1e18); + + assertGt(vault.totalFloat(), 0); + assertGt(vault.convertToAssets(10**vault.decimals()), 1.4e18); + assertLt(vault.convertToAssets(10**vault.decimals()), 1.5e18); } function testUnprofitableHarvest() public { underlying.mint(address(this), 1e18); underlying.approve(address(vault), 1e18); - vault.deposit(1e18); + vault.deposit(1e18, address(this)); vault.trustStrategy(strategy1); vault.depositIntoStrategy(strategy1, 1e18); - vault.pushToWithdrawalQueue(strategy1); + vault.pushToWithdrawalStack(strategy1); - assertEq(vault.exchangeRate(), 1e18); + assertEq(vault.convertToAssets(10**vault.decimals()), 1e18); assertEq(vault.totalStrategyHoldings(), 1e18); assertEq(vault.totalFloat(), 0); - assertEq(vault.totalHoldings(), 1e18); + assertEq(vault.totalAssets(), 1e18); assertEq(vault.balanceOf(address(this)), 1e18); - assertEq(vault.balanceOfUnderlying(address(this)), 1e18); + assertEq(vault.convertToAssets(vault.balanceOf(address(this))), 1e18); assertEq(vault.totalSupply(), 1e18); assertEq(vault.balanceOf(address(vault)), 0); - assertEq(vault.balanceOfUnderlying(address(vault)), 0); + assertEq(vault.convertToAssets(vault.balanceOf(address(vault))), 0); strategy1.simulateLoss(0.5e18); - assertEq(vault.exchangeRate(), 1e18); + assertEq(vault.convertToAssets(10**vault.decimals()), 1e18); assertEq(vault.totalStrategyHoldings(), 1e18); assertEq(vault.totalFloat(), 0); - assertEq(vault.totalHoldings(), 1e18); + assertEq(vault.totalAssets(), 1e18); assertEq(vault.balanceOf(address(this)), 1e18); - assertEq(vault.balanceOfUnderlying(address(this)), 1e18); + assertEq(vault.convertToAssets(vault.balanceOf(address(this))), 1e18); assertEq(vault.totalSupply(), 1e18); assertEq(vault.balanceOf(address(vault)), 0); - assertEq(vault.balanceOfUnderlying(address(vault)), 0); + assertEq(vault.convertToAssets(vault.balanceOf(address(vault))), 0); + + assertEq(vault.lastHarvest(), 0); + assertEq(vault.lastHarvestWindowStart(), 0); - vault.harvest(strategy1); + Strategy[] memory strategiesToHarvest = new Strategy[](1); + strategiesToHarvest[0] = strategy1; - assertEq(vault.exchangeRate(), 0.5e18); + vault.harvest(strategiesToHarvest); + + uint256 startingTimestamp = block.timestamp; + + assertEq(vault.lastHarvest(), startingTimestamp); + assertEq(vault.lastHarvestWindowStart(), startingTimestamp); + + assertEq(vault.convertToAssets(10**vault.decimals()), 0.5e18); assertEq(vault.totalStrategyHoldings(), 0.5e18); assertEq(vault.totalFloat(), 0); - assertEq(vault.totalHoldings(), 0.5e18); + assertEq(vault.totalAssets(), 0.5e18); assertEq(vault.balanceOf(address(this)), 1e18); - assertEq(vault.balanceOfUnderlying(address(this)), 0.5e18); + assertEq(vault.convertToAssets(vault.balanceOf(address(this))), 0.5e18); assertEq(vault.totalSupply(), 1e18); assertEq(vault.balanceOf(address(vault)), 0); - assertEq(vault.balanceOfUnderlying(address(vault)), 0); + assertEq(vault.convertToAssets(vault.balanceOf(address(vault))), 0); - vault.redeem(1e18); + vault.redeem(1e18, address(this), address(this)); assertEq(underlying.balanceOf(address(this)), 0.5e18); - assertEq(vault.exchangeRate(), 1e18); + assertEq(vault.convertToAssets(10**vault.decimals()), 1e18); assertEq(vault.totalStrategyHoldings(), 0); assertEq(vault.totalFloat(), 0); - assertEq(vault.totalHoldings(), 0); + assertEq(vault.totalAssets(), 0); assertEq(vault.balanceOf(address(this)), 0); - assertEq(vault.balanceOfUnderlying(address(this)), 0); + assertEq(vault.convertToAssets(vault.balanceOf(address(this))), 0); assertEq(vault.totalSupply(), 0); assertEq(vault.balanceOf(address(vault)), 0); - assertEq(vault.balanceOfUnderlying(address(vault)), 0); + assertEq(vault.convertToAssets(vault.balanceOf(address(vault))), 0); + } + + function testMultipleHarvestsInWindow() public { + underlying.mint(address(this), 1.5e18); + + underlying.approve(address(vault), 1e18); + vault.deposit(1e18, address(this)); + + vault.trustStrategy(strategy1); + vault.depositIntoStrategy(strategy1, 0.5e18); + + vault.trustStrategy(strategy2); + vault.depositIntoStrategy(strategy2, 0.5e18); + + underlying.transfer(address(strategy1), 0.25e18); + underlying.transfer(address(strategy2), 0.25e18); + + assertEq(vault.lastHarvest(), 0); + assertEq(vault.lastHarvestWindowStart(), 0); + + Strategy[] memory strategiesToHarvest = new Strategy[](2); + strategiesToHarvest[0] = strategy1; + strategiesToHarvest[1] = strategy2; + + vault.harvest(strategiesToHarvest); + + uint256 startingTimestamp = block.timestamp; + + assertEq(vault.lastHarvest(), startingTimestamp); + assertEq(vault.lastHarvestWindowStart(), startingTimestamp); + + hevm.warp(block.timestamp + (vault.harvestWindow() / 2)); + + uint256 exchangeRateBeforeHarvest = vault.convertToAssets(10**vault.decimals()); + + vault.harvest(strategiesToHarvest); + + assertEq(vault.convertToAssets(10**vault.decimals()), exchangeRateBeforeHarvest); + + assertEq(vault.lastHarvest(), block.timestamp); + assertEq(vault.lastHarvestWindowStart(), startingTimestamp); + } + + function testUpdatingHarvestDelay() public { + assertEq(vault.harvestDelay(), 6 hours); + assertEq(vault.nextHarvestDelay(), 0); + + vault.setHarvestDelay(12 hours); + + assertEq(vault.harvestDelay(), 6 hours); + assertEq(vault.nextHarvestDelay(), 12 hours); + + vault.trustStrategy(strategy1); + + Strategy[] memory strategiesToHarvest = new Strategy[](1); + strategiesToHarvest[0] = strategy1; + + vault.harvest(strategiesToHarvest); + + assertEq(vault.harvestDelay(), 12 hours); + assertEq(vault.nextHarvestDelay(), 0); + } + + function testClaimFees() public { + underlying.mint(address(this), 1e18); + + underlying.approve(address(vault), 1e18); + vault.deposit(1e18, address(this)); + + vault.transfer(address(vault), 1e18); + + assertEq(vault.balanceOf(address(vault)), 1e18); + assertEq(vault.balanceOf(address(this)), 0); + + vault.claimFees(1e18); + + assertEq(vault.balanceOf(address(vault)), 0); + assertEq(vault.balanceOf(address(this)), 1e18); + } + + /*/////////////////////////////////////////////////////////////// + HARVEST SANITY CHECK TESTS + //////////////////////////////////////////////////////////////*/ + + function testFailHarvestAfterWindowBeforeDelay() public { + underlying.mint(address(this), 1.5e18); + + underlying.approve(address(vault), 1e18); + vault.deposit(1e18, address(this)); + + vault.trustStrategy(strategy1); + vault.depositIntoStrategy(strategy1, 0.5e18); + + vault.trustStrategy(strategy2); + vault.depositIntoStrategy(strategy2, 0.5e18); + + Strategy[] memory strategiesToHarvest = new Strategy[](2); + strategiesToHarvest[0] = strategy1; + strategiesToHarvest[1] = strategy2; + + vault.harvest(strategiesToHarvest); + + hevm.warp(block.timestamp + vault.harvestWindow() + 1); + + vault.harvest(strategiesToHarvest); + } + + function testFailHarvestUntrustedStrategy() public { + underlying.mint(address(this), 1e18); + + underlying.approve(address(vault), 1e18); + vault.deposit(1e18, address(this)); + + vault.trustStrategy(strategy1); + vault.depositIntoStrategy(strategy1, 1e18); + + vault.distrustStrategy(strategy1); + + Strategy[] memory strategiesToHarvest = new Strategy[](1); + strategiesToHarvest[0] = strategy1; + + vault.harvest(strategiesToHarvest); + } + + /*/////////////////////////////////////////////////////////////// + WITHDRAWAL STACK TESTS + //////////////////////////////////////////////////////////////*/ + + function testPushingToWithdrawalStack() public { + vault.pushToWithdrawalStack(Strategy(address(69))); + vault.pushToWithdrawalStack(Strategy(address(420))); + vault.pushToWithdrawalStack(Strategy(address(1337))); + vault.pushToWithdrawalStack(Strategy(address(69420))); + + assertEq(vault.getWithdrawalStack().length, 4); + + assertEq(address(vault.withdrawalStack(0)), address(69)); + assertEq(address(vault.withdrawalStack(1)), address(420)); + assertEq(address(vault.withdrawalStack(2)), address(1337)); + assertEq(address(vault.withdrawalStack(3)), address(69420)); + } + + function testPoppingFromWithdrawalStack() public { + vault.pushToWithdrawalStack(Strategy(address(69))); + vault.pushToWithdrawalStack(Strategy(address(420))); + vault.pushToWithdrawalStack(Strategy(address(1337))); + vault.pushToWithdrawalStack(Strategy(address(69420))); + + vault.popFromWithdrawalStack(); + assertEq(vault.getWithdrawalStack().length, 3); + + vault.popFromWithdrawalStack(); + assertEq(vault.getWithdrawalStack().length, 2); + + vault.popFromWithdrawalStack(); + assertEq(vault.getWithdrawalStack().length, 1); + + vault.popFromWithdrawalStack(); + assertEq(vault.getWithdrawalStack().length, 0); + } + + function testReplaceWithdrawalStackIndex() public { + Strategy[] memory newStack = new Strategy[](4); + newStack[0] = Strategy(address(1)); + newStack[1] = Strategy(address(2)); + newStack[2] = Strategy(address(3)); + newStack[3] = Strategy(address(4)); + + vault.setWithdrawalStack(newStack); + + vault.replaceWithdrawalStackIndex(1, Strategy(address(420))); + + assertEq(vault.getWithdrawalStack().length, 4); + assertEq(address(vault.withdrawalStack(1)), address(420)); + } + + function testReplaceWithdrawalStackIndexWithTip() public { + Strategy[] memory newStack = new Strategy[](4); + newStack[0] = Strategy(address(1001)); + newStack[1] = Strategy(address(1002)); + newStack[2] = Strategy(address(1003)); + newStack[3] = Strategy(address(1004)); + + vault.setWithdrawalStack(newStack); + + vault.replaceWithdrawalStackIndexWithTip(1); + + assertEq(vault.getWithdrawalStack().length, 3); + assertEq(address(vault.withdrawalStack(2)), address(1003)); + assertEq(address(vault.withdrawalStack(1)), address(1004)); + } + + function testSwapWithdrawalStackIndexes() public { + Strategy[] memory newStack = new Strategy[](4); + newStack[0] = Strategy(address(1001)); + newStack[1] = Strategy(address(1002)); + newStack[2] = Strategy(address(1003)); + newStack[3] = Strategy(address(1004)); + + vault.setWithdrawalStack(newStack); + + vault.swapWithdrawalStackIndexes(1, 2); + + assertEq(vault.getWithdrawalStack().length, 4); + assertEq(address(vault.withdrawalStack(1)), address(1003)); + assertEq(address(vault.withdrawalStack(2)), address(1002)); + } + + function testFailPushStackFull() public { + Strategy[] memory fullStack = new Strategy[](32); + + vault.setWithdrawalStack(fullStack); + + vault.pushToWithdrawalStack(Strategy(address(69))); + } + + function testFailSetStackTooBig() public { + Strategy[] memory tooBigStack = new Strategy[](33); + + vault.setWithdrawalStack(tooBigStack); + } + + function testFailPopStackEmpty() public { + vault.popFromWithdrawalStack(); } /*/////////////////////////////////////////////////////////////// EDGE CASE TESTS //////////////////////////////////////////////////////////////*/ + function testWithdrawingWithDuplicateStrategiesInStack() public { + underlying.mint(address(this), 1e18); + + underlying.approve(address(vault), 1e18); + vault.deposit(1e18, address(this)); + + vault.trustStrategy(strategy1); + vault.depositIntoStrategy(strategy1, 0.5e18); + + vault.trustStrategy(strategy2); + vault.depositIntoStrategy(strategy2, 0.5e18); + + vault.pushToWithdrawalStack(strategy1); + vault.pushToWithdrawalStack(strategy1); + vault.pushToWithdrawalStack(strategy2); + vault.pushToWithdrawalStack(strategy1); + vault.pushToWithdrawalStack(strategy1); + + assertEq(vault.getWithdrawalStack().length, 5); + + vault.redeem(1e18, address(this), address(this)); + + assertEq(vault.getWithdrawalStack().length, 2); + + assertEq(address(vault.withdrawalStack(0)), address(strategy1)); + assertEq(address(vault.withdrawalStack(1)), address(strategy1)); + } + + function testWithdrawingWithUntrustedStrategyInStack() public { + underlying.mint(address(this), 1e18); + + underlying.approve(address(vault), 1e18); + vault.deposit(1e18, address(this)); + + vault.trustStrategy(strategy1); + vault.depositIntoStrategy(strategy1, 0.5e18); + + vault.trustStrategy(strategy2); + vault.depositIntoStrategy(strategy2, 0.5e18); + + vault.pushToWithdrawalStack(strategy2); + vault.pushToWithdrawalStack(strategy2); + vault.pushToWithdrawalStack(new MockERC20Strategy(underlying)); + vault.pushToWithdrawalStack(strategy1); + vault.pushToWithdrawalStack(strategy1); + + assertEq(vault.getWithdrawalStack().length, 5); + + vault.redeem(1e18, address(this), address(this)); + + assertEq(vault.getWithdrawalStack().length, 1); + + assertEq(address(vault.withdrawalStack(0)), address(strategy2)); + } + function testFailTrustStrategyWithWrongUnderlying() public { MockERC20 wrongUnderlying = new MockERC20("Not The Right Token", "TKN2", 18); - MockStrategy badStrategy = new MockStrategy(wrongUnderlying); + MockERC20Strategy badStrategy = new MockERC20Strategy(wrongUnderlying); vault.trustStrategy(badStrategy); } - function testFailWithdrawWithEmptyQueue() public { + function testFailTrustStrategyWithETHUnderlying() public { + MockETHStrategy ethStrategy = new MockETHStrategy(); + + vault.trustStrategy(ethStrategy); + } + + function testFailWithdrawWithEmptyStack() public { underlying.mint(address(this), 1e18); underlying.approve(address(vault), 1e18); - vault.deposit(1e18); + vault.deposit(1e18, address(this)); vault.trustStrategy(strategy1); vault.depositIntoStrategy(strategy1, 1e18); - vault.redeem(1e18); + vault.redeem(1e18, address(this), address(this)); } - function testFailWithdrawWithIncompleteQueue() public { + function testFailWithdrawWithIncompleteStack() public { underlying.mint(address(this), 1e18); underlying.approve(address(vault), 1e18); - vault.deposit(1e18); + vault.deposit(1e18, address(this)); vault.trustStrategy(strategy1); vault.depositIntoStrategy(strategy1, 0.5e18); - vault.pushToWithdrawalQueue(strategy1); + vault.pushToWithdrawalStack(strategy1); vault.trustStrategy(strategy2); vault.depositIntoStrategy(strategy2, 0.5e18); - vault.redeem(1e18); + vault.redeem(1e18, address(this), address(this)); } - // function testUpdatingProfitUnlockDelayWhileProfitIsStillLocked() public { - // underlying.mint(address(this), 1.5e18); + function testFailInitializeTwice() public { + vault.initialize(); + } + + function testDestroyVault() public { + vault.destroy(); + } +} - // underlying.approve(address(vault), 1e18); - // vault.deposit(1e18); +contract VaultsETHTest is DSTestPlus { + Vault wethVault; + WETH weth; - // vault.trustStrategy(strategy1); - // vault.depositIntoStrategy(strategy1, 1e18); - // vault.pushToWithdrawalQueue(strategy1); + MockETHStrategy ethStrategy; + MockERC20Strategy erc20Strategy; - // underlying.transfer(address(strategy1), 0.5e18); - // vault.harvest(strategy1); + function setUp() public { + weth = new WETH(); - // hevm.warp(block.timestamp + (vault.harvestDelay() / 2)); - // assertEq(vault.balanceOfUnderlying(address(this)), 1.25e18); + wethVault = new VaultFactory(address(this), Authority(address(0))).deployVault(weth); - // vault.setHarvestDelay(vault.harvestDelay() * 2); - // assertEq(vault.balanceOfUnderlying(address(this)), 1.125e18); + wethVault.setFeePercent(0.1e18); + wethVault.setHarvestDelay(6 hours); + wethVault.setHarvestWindow(5 minutes); + wethVault.setTargetFloatPercent(0.01e18); - // hevm.warp(block.timestamp + vault.harvestDelay()); - // assertEq(vault.balanceOfUnderlying(address(this)), 1.5e18); + wethVault.setUnderlyingIsWETH(true); - // vault.redeem(1e18); - // assertEq(underlying.balanceOf(address(this)), 1.5e18); - // } + wethVault.initialize(); - /*/////////////////////////////////////////////////////////////// - WITHDRAWAL QUEUE TESTS - //////////////////////////////////////////////////////////////*/ + ethStrategy = new MockETHStrategy(); + erc20Strategy = new MockERC20Strategy(weth); + } - function testPushingToWithdrawalQueue() public { - vault.pushToWithdrawalQueue(Strategy(address(69))); - vault.pushToWithdrawalQueue(Strategy(address(420))); - vault.pushToWithdrawalQueue(Strategy(address(1337))); - vault.pushToWithdrawalQueue(Strategy(address(69420))); + function testAtomicDepositWithdrawIntoETHStrategies() public { + uint256 startingETHBal = address(this).balance; - assertEq(vault.getWithdrawalQueue().length, 4); + weth.deposit{value: 1 ether}(); - assertEq(address(vault.withdrawalQueue(0)), address(69)); - assertEq(address(vault.withdrawalQueue(1)), address(420)); - assertEq(address(vault.withdrawalQueue(2)), address(1337)); - assertEq(address(vault.withdrawalQueue(3)), address(69420)); - } + assertEq(address(this).balance, startingETHBal - 1 ether); - function testPoppingFromWithdrawalQueue() public { - vault.pushToWithdrawalQueue(Strategy(address(69))); - vault.pushToWithdrawalQueue(Strategy(address(420))); - vault.pushToWithdrawalQueue(Strategy(address(1337))); - vault.pushToWithdrawalQueue(Strategy(address(69420))); + weth.approve(address(wethVault), 1e18); + wethVault.deposit(1e18, address(this)); - vault.popFromWithdrawalQueue(); + wethVault.trustStrategy(ethStrategy); + wethVault.depositIntoStrategy(ethStrategy, 0.5e18); + wethVault.pushToWithdrawalStack(ethStrategy); - assertEq(vault.getWithdrawalQueue().length, 3); + wethVault.trustStrategy(erc20Strategy); + wethVault.depositIntoStrategy(erc20Strategy, 0.5e18); + wethVault.pushToWithdrawalStack(erc20Strategy); - vault.popFromWithdrawalQueue(); + wethVault.withdrawFromStrategy(ethStrategy, 0.25e18); + wethVault.withdrawFromStrategy(erc20Strategy, 0.25e18); - assertEq(vault.getWithdrawalQueue().length, 2); + wethVault.redeem(1e18, address(this), address(this)); - vault.popFromWithdrawalQueue(); + weth.withdraw(1 ether); - assertEq(vault.getWithdrawalQueue().length, 1); + assertEq(address(this).balance, startingETHBal); + } - vault.popFromWithdrawalQueue(); + function testTrustStrategyWithETHUnderlying() public { + wethVault.trustStrategy(ethStrategy); - assertEq(vault.getWithdrawalQueue().length, 0); + (bool trusted, ) = wethVault.getStrategyData(ethStrategy); + assertTrue(trusted); } - function testReplaceWithdrawalQueueIndex() public { - Strategy[] memory newQueue = new Strategy[](4); - newQueue[0] = Strategy(address(1)); - newQueue[1] = Strategy(address(2)); - newQueue[2] = Strategy(address(3)); - newQueue[3] = Strategy(address(4)); + function testTrustStrategyWithWETHUnderlying() public { + wethVault.trustStrategy(erc20Strategy); - vault.setWithdrawalQueue(newQueue); + (bool trusted, ) = wethVault.getStrategyData(erc20Strategy); + assertTrue(trusted); + } - vault.replaceWithdrawalQueueIndex(1, Strategy(address(420))); + function testDestroyVaultReturnsETH() public { + uint256 startingETHBal = address(this).balance; + payable(address(wethVault)).transfer(1 ether); - assertEq(vault.getWithdrawalQueue().length, 4); - assertEq(address(vault.withdrawalQueue(1)), address(420)); + wethVault.destroy(); + assertEq(address(this).balance, startingETHBal); } - function testReplaceWithdrawalQueueIndexWithTip() public { - Strategy[] memory newQueue = new Strategy[](4); - newQueue[0] = Strategy(address(1001)); - newQueue[1] = Strategy(address(1002)); - newQueue[2] = Strategy(address(1003)); - newQueue[3] = Strategy(address(1004)); - vault.setWithdrawalQueue(newQueue); + receive() external payable {} +} - vault.replaceWithdrawalQueueIndexWithTip(1); +contract UnInitializedVaultTest is DSTestPlus { + Vault vault; + MockERC20 underlying; - assertEq(vault.getWithdrawalQueue().length, 3); - assertEq(address(vault.withdrawalQueue(2)), address(1003)); - assertEq(address(vault.withdrawalQueue(1)), address(1004)); + function setUp() public { + underlying = new MockERC20("Mock Token", "TKN", 18); + + vault = new VaultFactory(address(this), Authority(address(0))).deployVault(underlying); + + vault.setFeePercent(0.1e18); + vault.setHarvestDelay(6 hours); + vault.setHarvestWindow(5 minutes); + vault.setTargetFloatPercent(0.01e18); } - function testSwapWithdrawalQueueIndexes() public { - Strategy[] memory newQueue = new Strategy[](4); - newQueue[0] = Strategy(address(1001)); - newQueue[1] = Strategy(address(1002)); - newQueue[2] = Strategy(address(1003)); - newQueue[3] = Strategy(address(1004)); - vault.setWithdrawalQueue(newQueue); + function testFailDeposit() public { + underlying.mint(address(this), 1e18); - vault.swapWithdrawalQueueIndexes(1, 2); + underlying.approve(address(vault), 1e18); + vault.deposit(1e18, address(this)); + } + + function testInitializeAndDeposit() public { + assertFalse(vault.isInitialized()); + assertEq(vault.totalSupply(), type(uint256).max); + + vault.initialize(); + + assertTrue(vault.isInitialized()); + assertEq(vault.totalSupply(), 0); + + underlying.mint(address(this), 1e18); - assertEq(vault.getWithdrawalQueue().length, 4); - assertEq(address(vault.withdrawalQueue(1)), address(1003)); - assertEq(address(vault.withdrawalQueue(2)), address(1002)); + underlying.approve(address(vault), 1e18); + vault.deposit(1e18, address(this)); } } + +// Bound a value between a min and max. +function bound( + uint256 x, + uint256 min, + uint256 max +) pure returns (uint256 result) { + require(max >= min, "MAX_LESS_THAN_MIN"); + + uint256 size = max - min; + + if (max != type(uint256).max) size++; // Make the max inclusive. + if (size == 0) return min; // Using max would be equivalent as well. + // Ensure max is inclusive in cases where x != 0 and max is at uint max. + if (max == type(uint256).max && x != 0) x--; // Accounted for later. + + if (x < min) x += size * (((min - x) / size) + 1); + result = min + ((x - min) % size); + + // Account for decrementing x to make max inclusive. + if (max == type(uint256).max && x != 0) result++; +} + +function getDiff(uint256 a, uint256 b) pure returns (uint256) { + return a >= b ? a - b : b - a; +} diff --git a/src/test/VaultConfigurationModule.t.sol b/src/test/VaultConfigurationModule.t.sol new file mode 100644 index 0000000..6fc67de --- /dev/null +++ b/src/test/VaultConfigurationModule.t.sol @@ -0,0 +1,223 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.10; + +import {Authority} from "solmate/auth/Auth.sol"; +import {DSTestPlus} from "solmate/test/utils/DSTestPlus.sol"; +import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; +import {MockAuthority} from "solmate/test/utils/mocks/MockAuthority.sol"; + +import {VaultConfigurationModule} from "../modules/VaultConfigurationModule.sol"; + +import {Vault} from "../Vault.sol"; +import {VaultFactory} from "../VaultFactory.sol"; + +contract VaultConfigurationModuleTest is DSTestPlus { + VaultFactory vaultFactory; + + Vault vault; + MockERC20 underlying; + + VaultConfigurationModule vaultConfigurationModule; + + function setUp() public { + vaultConfigurationModule = new VaultConfigurationModule(address(this), Authority(address(0))); + + vaultFactory = new VaultFactory(address(this), new MockAuthority(true)); + + underlying = new MockERC20("Mock Token", "TKN", 18); + + vault = vaultFactory.deployVault(underlying); + } + + /*/////////////////////////////////////////////////////////////// + DEFAULT SETTER TESTS + //////////////////////////////////////////////////////////////*/ + + function testSetDefaultFeePercent() public { + vaultConfigurationModule.setDefaultFeePercent(0.1e18); + + assertEq(vaultConfigurationModule.defaultFeePercent(), 0.1e18); + + vaultConfigurationModule.syncFeePercent(vault); + + assertEq(vault.feePercent(), 0.1e18); + } + + function testSetDefaultHarvestDelay() public { + vaultConfigurationModule.setDefaultHarvestDelay(6 hours); + + assertEq(vaultConfigurationModule.defaultHarvestDelay(), 6 hours); + + vaultConfigurationModule.syncHarvestDelay(vault); + + assertEq(vault.harvestDelay(), 6 hours); + } + + function testSetDefaultHarvestWindow() public { + // Harvest delay has to be set before harvest window. + vaultConfigurationModule.setDefaultHarvestDelay(6 hours); + vaultConfigurationModule.syncHarvestDelay(vault); + + vaultConfigurationModule.setDefaultHarvestWindow(5 minutes); + + assertEq(vaultConfigurationModule.defaultHarvestWindow(), 5 minutes); + + vaultConfigurationModule.syncHarvestWindow(vault); + + assertEq(vault.harvestWindow(), 5 minutes); + } + + function testSetDefaultTargetFloatPercent() public { + vaultConfigurationModule.setDefaultTargetFloatPercent(0.01e18); + + assertEq(vaultConfigurationModule.defaultTargetFloatPercent(), 0.01e18); + + vaultConfigurationModule.syncTargetFloatPercent(vault); + + assertEq(vault.targetFloatPercent(), 0.01e18); + } + + /*/////////////////////////////////////////////////////////////// + CUSTOM SETTER TESTS + //////////////////////////////////////////////////////////////*/ + + function testSetCustomFeePercent() public { + vaultConfigurationModule.setVaultCustomFeePercent(vault, 0.1e18); + + assertEq(vaultConfigurationModule.getVaultCustomFeePercent(vault), 0.1e18); + + vaultConfigurationModule.syncFeePercent(vault); + + assertEq(vault.feePercent(), 0.1e18); + } + + function testSetCustomHarvestDelay() public { + vaultConfigurationModule.setVaultCustomHarvestDelay(vault, 6 hours); + + assertEq(vaultConfigurationModule.getVaultCustomHarvestDelay(vault), 6 hours); + + vaultConfigurationModule.syncHarvestDelay(vault); + + assertEq(vault.harvestDelay(), 6 hours); + } + + function testSetCustomHarvestWindow() public { + // Harvest delay has to be set before harvest window. + vaultConfigurationModule.setVaultCustomHarvestDelay(vault, 6 hours); + vaultConfigurationModule.syncHarvestDelay(vault); + + vaultConfigurationModule.setVaultCustomHarvestWindow(vault, 5 minutes); + + assertEq(vaultConfigurationModule.getVaultCustomHarvestWindow(vault), 5 minutes); + + vaultConfigurationModule.syncHarvestWindow(vault); + + assertEq(vault.harvestWindow(), 5 minutes); + } + + function testSetCustomTargetFloatPercent() public { + vaultConfigurationModule.setVaultCustomTargetFloatPercent(vault, 0.01e18); + + assertEq(vaultConfigurationModule.getVaultCustomTargetFloatPercent(vault), 0.01e18); + + vaultConfigurationModule.syncTargetFloatPercent(vault); + + assertEq(vault.targetFloatPercent(), 0.01e18); + } + + /*/////////////////////////////////////////////////////////////// + CUSTOM OVERRIDES DEFAULT TESTS + //////////////////////////////////////////////////////////////*/ + + function tesCustomFeePercentOverridesDefault() public { + vaultConfigurationModule.setVaultCustomFeePercent(vault, 0.1e18); + + vaultConfigurationModule.setDefaultFeePercent(0.2e18); + + vaultConfigurationModule.syncFeePercent(vault); + + assertEq(vault.feePercent(), 0.1e18); + } + + function testCustomHarvestDelayOverridesDefault() public { + vaultConfigurationModule.setVaultCustomHarvestDelay(vault, 6 hours); + + vaultConfigurationModule.setDefaultHarvestDelay(5 hours); + + vaultConfigurationModule.syncHarvestDelay(vault); + + assertEq(vault.harvestDelay(), 6 hours); + } + + function testCustomHarvestWindowOverridesDefault() public { + // Harvest delay has to be set before harvest window. + vaultConfigurationModule.setDefaultHarvestDelay(6 hours); + vaultConfigurationModule.syncHarvestDelay(vault); + + vaultConfigurationModule.setVaultCustomHarvestWindow(vault, 5 minutes); + + vaultConfigurationModule.setDefaultHarvestWindow(10 minutes); + + vaultConfigurationModule.syncHarvestWindow(vault); + + assertEq(vault.harvestWindow(), 5 minutes); + } + + function testCustomTargetFloatPercentOverridesDefault() public { + vaultConfigurationModule.setVaultCustomTargetFloatPercent(vault, 0.01e18); + + vaultConfigurationModule.setDefaultTargetFloatPercent(0.02e18); + + vaultConfigurationModule.syncTargetFloatPercent(vault); + + assertEq(vault.targetFloatPercent(), 0.01e18); + } + + /*/////////////////////////////////////////////////////////////// + DEFAULT OVERRIDES CUSTOM OF ZERO TESTS + //////////////////////////////////////////////////////////////*/ + + function testDefaultFeePercentOverridesCustomOfZero() public { + vaultConfigurationModule.setDefaultFeePercent(0.1e18); + + vaultConfigurationModule.setVaultCustomFeePercent(vault, 0); + + vaultConfigurationModule.syncFeePercent(vault); + + assertEq(vault.feePercent(), 0.1e18); + } + + function testDefaultHarvestDelayOverridesCustomOfZero() public { + vaultConfigurationModule.setDefaultHarvestDelay(6 hours); + + vaultConfigurationModule.setVaultCustomHarvestDelay(vault, 0); + + vaultConfigurationModule.syncHarvestDelay(vault); + + assertEq(vault.harvestDelay(), 6 hours); + } + + function testDefaultHarvestWindowOverridesCustomOfZero() public { + // Harvest delay has to be set before harvest window. + vaultConfigurationModule.setDefaultHarvestDelay(6 hours); + vaultConfigurationModule.syncHarvestDelay(vault); + + vaultConfigurationModule.setDefaultHarvestWindow(5 minutes); + + vaultConfigurationModule.setVaultCustomHarvestWindow(vault, 0); + + vaultConfigurationModule.syncHarvestWindow(vault); + + assertEq(vault.harvestWindow(), 5 minutes); + } + + function testDefaultTargetFloatPercentOverridesCustomOfZero() public { + vaultConfigurationModule.setDefaultTargetFloatPercent(0.01e18); + + vaultConfigurationModule.setVaultCustomTargetFloatPercent(vault, 0); + + vaultConfigurationModule.syncTargetFloatPercent(vault); + + assertEq(vault.targetFloatPercent(), 0.01e18); + } +} diff --git a/src/test/VaultETHWrapperModule.t.sol b/src/test/VaultETHWrapperModule.t.sol deleted file mode 100644 index 93143a2..0000000 --- a/src/test/VaultETHWrapperModule.t.sol +++ /dev/null @@ -1,93 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -pragma solidity 0.8.9; - -import {WETH} from "solmate/tokens/WETH.sol"; - -import {DSTestPlus} from "./utils/DSTestPlus.sol"; - -import {VaultETHWrapperModule} from "../modules/VaultETHWrapperModule.sol"; - -import {Vault} from "../Vault.sol"; -import {VaultFactory} from "../VaultFactory.sol"; - -contract VaultETHWrapperModuleTest is DSTestPlus { - Vault vault; - WETH underlying; - - VaultETHWrapperModule vaultETHWrapperModule; - - function setUp() public { - underlying = new WETH(); - vault = new VaultFactory().deployVault(underlying); - - vault.initialize(); - - vaultETHWrapperModule = new VaultETHWrapperModule(); - } - - function testAtomicDepositWithdrawETH() public { - vault.setUnderlyingIsWETH(true); - - uint256 startingETHBal = address(this).balance; - - vaultETHWrapperModule.depositETHIntoVault{value: 1 ether}(vault); - - assertEq(address(this).balance, startingETHBal - 1 ether); - - assertEq(vault.balanceOf(address(this)), 1e18); - assertEq(vault.balanceOfUnderlying(address(this)), 1 ether); - - vault.approve(address(vaultETHWrapperModule), 1e18); - vaultETHWrapperModule.withdrawETHFromVault(vault, 1 ether); - - assertEq(address(this).balance, startingETHBal); - } - - function testAtomicDepositRedeemETH() public { - vault.setUnderlyingIsWETH(true); - - uint256 startingETHBal = address(this).balance; - - vaultETHWrapperModule.depositETHIntoVault{value: 69 ether}(vault); - - assertEq(address(this).balance, startingETHBal - 69 ether); - - assertEq(vault.balanceOf(address(this)), 69e18); - assertEq(vault.balanceOfUnderlying(address(this)), 69 ether); - - vault.approve(address(vaultETHWrapperModule), 69e19); - vaultETHWrapperModule.redeemETHFromVault(vault, 69e18); - - assertEq(address(this).balance, startingETHBal); - } - - function testFailDepositIntoNotWETHVault() public { - vaultETHWrapperModule.depositETHIntoVault{value: 1 ether}(vault); - } - - function testFailWithdrawFromNotWETHVault() public { - vault.setUnderlyingIsWETH(true); - - vaultETHWrapperModule.depositETHIntoVault{value: 1 ether}(vault); - - vault.setUnderlyingIsWETH(false); - - vault.approve(address(vaultETHWrapperModule), 1e18); - - vaultETHWrapperModule.withdrawETHFromVault(vault, 1 ether); - } - - function testFailRedeemFromNotWETHVault() public { - vault.setUnderlyingIsWETH(true); - - vaultETHWrapperModule.depositETHIntoVault{value: 1 ether}(vault); - - vault.setUnderlyingIsWETH(false); - - vault.approve(address(vaultETHWrapperModule), 1e18); - - vaultETHWrapperModule.redeemETHFromVault(vault, 1e18); - } - - receive() external payable {} -} diff --git a/src/test/VaultFactory.t.sol b/src/test/VaultFactory.t.sol index e32eb7f..ce0f690 100644 --- a/src/test/VaultFactory.t.sol +++ b/src/test/VaultFactory.t.sol @@ -1,32 +1,38 @@ // SPDX-License-Identifier: AGPL-3.0-only -pragma solidity 0.8.9; +pragma solidity 0.8.10; +import {Authority} from "solmate/auth/Auth.sol"; +import {DSTestPlus} from "solmate/test/utils/DSTestPlus.sol"; import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; -import {DSTestPlus} from "./utils/DSTestPlus.sol"; - import {Vault} from "../Vault.sol"; import {VaultFactory} from "../VaultFactory.sol"; contract VaultFactoryTest is DSTestPlus { VaultFactory vaultFactory; + MockERC20 underlying; function setUp() public { - vaultFactory = new VaultFactory(); underlying = new MockERC20("Mock Token", "TKN", 18); + + vaultFactory = new VaultFactory(address(this), Authority(address(0))); } function testDeployVault() public { Vault vault = vaultFactory.deployVault(underlying); - assertTrue(vaultFactory.isVaultDeployed(vault)); - assertVaultEq(vaultFactory.getVaultFromUnderlying(underlying), vault); - assertERC20Eq(vault.UNDERLYING(), underlying); + assertTrue(vaultFactory.isVaultDeployed(vault)); + assertEq(address(vaultFactory.getVaultFromUnderlying(underlying)), address(vault)); + assertEq(address(vault.UNDERLYING()), address(underlying)); } function testFailNoDuplicateVaults() public { vaultFactory.deployVault(underlying); vaultFactory.deployVault(underlying); } + + function testIsVaultDeployed() public { + assertFalse(vaultFactory.isVaultDeployed(Vault(payable(address(0xBEEF))))); + } } diff --git a/src/test/VaultInitializationModule.t.sol b/src/test/VaultInitializationModule.t.sol new file mode 100644 index 0000000..a30252f --- /dev/null +++ b/src/test/VaultInitializationModule.t.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.10; + +import {Authority} from "solmate/auth/Auth.sol"; +import {DSTestPlus} from "solmate/test/utils/DSTestPlus.sol"; +import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; +import {MockAuthority} from "solmate/test/utils/mocks/MockAuthority.sol"; + +import {VaultInitializationModule} from "../modules/VaultInitializationModule.sol"; +import {VaultConfigurationModule} from "../modules/VaultConfigurationModule.sol"; + +import {Vault} from "../Vault.sol"; +import {VaultFactory} from "../VaultFactory.sol"; + +contract VaultInitializationModuleTest is DSTestPlus { + VaultFactory vaultFactory; + + VaultConfigurationModule vaultConfigurationModule; + + VaultInitializationModule vaultInitializationModule; + + MockERC20 underlying; + + function setUp() public { + underlying = new MockERC20("Mock Token", "TKN", 18); + + vaultFactory = new VaultFactory(address(this), new MockAuthority(true)); + + vaultConfigurationModule = new VaultConfigurationModule(address(this), Authority(address(0))); + + vaultInitializationModule = new VaultInitializationModule( + vaultConfigurationModule, + address(this), + Authority(address(0)) + ); + } + + function testVaultCreation() public { + vaultConfigurationModule.setDefaultFeePercent(0.1e18); + vaultConfigurationModule.setDefaultHarvestDelay(6 hours); + vaultConfigurationModule.setDefaultHarvestWindow(5 minutes); + vaultConfigurationModule.setDefaultTargetFloatPercent(0.01e18); + + Vault vault = vaultFactory.deployVault(underlying); + + assertFalse(vault.isInitialized()); + assertEq(vault.feePercent(), 0); + assertEq(vault.harvestDelay(), 0); + assertEq(vault.harvestWindow(), 0); + assertEq(vault.targetFloatPercent(), 0); + + vaultInitializationModule.initializeVault(vault); + + assertTrue(vault.isInitialized()); + assertEq(vault.feePercent(), 0.1e18); + assertEq(vault.harvestDelay(), 6 hours); + assertEq(vault.harvestWindow(), 5 minutes); + assertEq(vault.targetFloatPercent(), 0.01e18); + } + + function testSetConfigurationModule() public { + vaultInitializationModule.setConfigModule(VaultConfigurationModule(address(0xBEEF))); + + assertEq(address(vaultInitializationModule.configModule()), address(0xBEEF)); + } +} diff --git a/src/test/VaultRouterModule.t.sol b/src/test/VaultRouterModule.t.sol new file mode 100644 index 0000000..d3e2065 --- /dev/null +++ b/src/test/VaultRouterModule.t.sol @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.10; + +import {WETH} from "solmate/tokens/WETH.sol"; +import {Authority} from "solmate/auth/Auth.sol"; +import {DSTestPlus} from "solmate/test/utils/DSTestPlus.sol"; + +import {VaultRouterModule} from "../modules/VaultRouterModule.sol"; + +import {Vault} from "../Vault.sol"; +import {VaultFactory} from "../VaultFactory.sol"; + +contract VaultRouterModuleTest is DSTestPlus { + Vault wethVault; + WETH weth; + + VaultRouterModule vaultRouterModule; + + function setUp() public { + weth = new WETH(); + + wethVault = new VaultFactory(address(this), Authority(address(0))).deployVault(weth); + + wethVault.initialize(); + + vaultRouterModule = new VaultRouterModule(); + } + + /*/////////////////////////////////////////////////////////////// + ETH DEPOSIT/WITHDRAWAL TESTS + //////////////////////////////////////////////////////////////*/ + + function testAtomicDepositWithdrawETH() public { + wethVault.setUnderlyingIsWETH(true); + + uint256 startingETHBal = address(this).balance; + + vaultRouterModule.depositETHIntoVault{value: 1 ether}(wethVault); + + assertEq(address(this).balance, startingETHBal - 1 ether); + + assertEq(wethVault.balanceOf(address(this)), 1e18); + assertEq(wethVault.convertToAssets((10**wethVault.decimals())), 1 ether); + + wethVault.approve(address(vaultRouterModule), 1e18); + vaultRouterModule.withdrawETHFromVault(wethVault, 1 ether); + + assertEq(address(this).balance, startingETHBal); + } + + function testAtomicDepositRedeemETH() public { + wethVault.setUnderlyingIsWETH(true); + + uint256 startingETHBal = address(this).balance; + + vaultRouterModule.depositETHIntoVault{value: 69 ether}(wethVault); + + assertEq(address(this).balance, startingETHBal - 69 ether); + + assertEq(wethVault.balanceOf(address(this)), 69e18); + assertEq((wethVault.convertToAssets(wethVault.balanceOf(address(this)))), 69 ether); + + wethVault.approve(address(vaultRouterModule), 69e19); + vaultRouterModule.redeemETHFromVault(wethVault, 69e18); + + assertEq(address(this).balance, startingETHBal); + } + + /*/////////////////////////////////////////////////////////////// + ETH DEPOSIT/WITHDRAWAL SANITY CHECK TESTS + //////////////////////////////////////////////////////////////*/ + + function testFailDepositIntoNotWETHVault() public { + vaultRouterModule.depositETHIntoVault{value: 1 ether}(wethVault); + } + + function testFailWithdrawFromNotWETHVault() public { + wethVault.setUnderlyingIsWETH(true); + + vaultRouterModule.depositETHIntoVault{value: 1 ether}(wethVault); + + wethVault.setUnderlyingIsWETH(false); + + wethVault.approve(address(vaultRouterModule), 1e18); + + vaultRouterModule.withdrawETHFromVault(wethVault, 1 ether); + } + + function testFailRedeemFromNotWETHVault() public { + wethVault.setUnderlyingIsWETH(true); + + vaultRouterModule.depositETHIntoVault{value: 1 ether}(wethVault); + + wethVault.setUnderlyingIsWETH(false); + + wethVault.approve(address(vaultRouterModule), 1e18); + + vaultRouterModule.redeemETHFromVault(wethVault, 1e18); + } + + receive() external payable {} +} diff --git a/src/test/mocks/MockStrategy.sol b/src/test/mocks/MockERC20Strategy.sol similarity index 74% rename from src/test/mocks/MockStrategy.sol rename to src/test/mocks/MockERC20Strategy.sol index df91e1b..a80d105 100644 --- a/src/test/mocks/MockStrategy.sol +++ b/src/test/mocks/MockERC20Strategy.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: AGPL-3.0-only -pragma solidity 0.8.9; +pragma solidity 0.8.10; import {ERC20} from "solmate/tokens/ERC20.sol"; import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; @@ -7,20 +7,22 @@ import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; import {ERC20Strategy} from "../../interfaces/Strategy.sol"; -contract MockStrategy is ERC20("Mock cToken Strategy", "cMOCK", 18), ERC20Strategy { +import "forge-std/console.sol"; + +contract MockERC20Strategy is ERC20("Mock cERC20 Strategy", "cERC20", 18), ERC20Strategy { using SafeTransferLib for ERC20; using FixedPointMathLib for uint256; - /*/////////////////////////////////////////////////////////////// - STRATEGY FUNCTIONS - //////////////////////////////////////////////////////////////*/ - constructor(ERC20 _UNDERLYING) { UNDERLYING = _UNDERLYING; BASE_UNIT = 10**_UNDERLYING.decimals(); } + /*/////////////////////////////////////////////////////////////// + STRATEGY LOGIC + //////////////////////////////////////////////////////////////*/ + function isCEther() external pure override returns (bool) { return false; } @@ -30,7 +32,7 @@ contract MockStrategy is ERC20("Mock cToken Strategy", "cMOCK", 18), ERC20Strate } function mint(uint256 amount) external override returns (uint256) { - _mint(msg.sender, amount.fdiv(exchangeRate(), BASE_UNIT)); + _mint(msg.sender, amount.mulDivDown(BASE_UNIT, exchangeRate())); UNDERLYING.safeTransferFrom(msg.sender, address(this), amount); @@ -38,19 +40,19 @@ contract MockStrategy is ERC20("Mock cToken Strategy", "cMOCK", 18), ERC20Strate } function redeemUnderlying(uint256 amount) external override returns (uint256) { - _burn(msg.sender, amount.fdiv(exchangeRate(), BASE_UNIT)); + _burn(msg.sender, amount.mulDivDown(BASE_UNIT, exchangeRate())); UNDERLYING.safeTransfer(msg.sender, amount); return 0; } - function balanceOfUnderlying(address user) external view override returns (uint256) { - return balanceOf[user].fmul(exchangeRate(), BASE_UNIT); + function balanceOfUnderlying(address) external view override returns (uint256) { + return UNDERLYING.balanceOf(address(this)); } /*/////////////////////////////////////////////////////////////// - INTERNAL LOGIC + INTERNAL LOGIC //////////////////////////////////////////////////////////////*/ ERC20 internal immutable UNDERLYING; @@ -62,11 +64,11 @@ contract MockStrategy is ERC20("Mock cToken Strategy", "cMOCK", 18), ERC20Strate if (cTokenSupply == 0) return BASE_UNIT; - return UNDERLYING.balanceOf(address(this)).fdiv(cTokenSupply, BASE_UNIT); + return UNDERLYING.balanceOf(address(this)).mulDivDown(BASE_UNIT, cTokenSupply); } /*/////////////////////////////////////////////////////////////// - MOCK FUNCTIONS + MOCK LOGIC //////////////////////////////////////////////////////////////*/ function simulateLoss(uint256 underlyingAmount) external { diff --git a/src/test/mocks/MockETHStrategy.sol b/src/test/mocks/MockETHStrategy.sol index 0edb32b..f68ad3a 100644 --- a/src/test/mocks/MockETHStrategy.sol +++ b/src/test/mocks/MockETHStrategy.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: AGPL-3.0-only -pragma solidity 0.8.9; +pragma solidity 0.8.10; import {ERC20} from "solmate/tokens/ERC20.sol"; import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; @@ -12,7 +12,7 @@ contract MockETHStrategy is ERC20("Mock cEther Strategy", "cEther", 18), ETHStra using FixedPointMathLib for uint256; /*/////////////////////////////////////////////////////////////// - STRATEGY FUNCTIONS + STRATEGY LOGIC //////////////////////////////////////////////////////////////*/ function isCEther() external pure override returns (bool) { @@ -20,11 +20,11 @@ contract MockETHStrategy is ERC20("Mock cEther Strategy", "cEther", 18), ETHStra } function mint() external payable override { - _mint(msg.sender, msg.value.fdiv(exchangeRate(), 1e18)); + _mint(msg.sender, msg.value.divWadDown(exchangeRate())); } function redeemUnderlying(uint256 amount) external override returns (uint256) { - _burn(msg.sender, amount.fdiv(exchangeRate(), 1e18)); + _burn(msg.sender, amount.divWadDown(exchangeRate())); msg.sender.safeTransferETH(amount); @@ -32,11 +32,11 @@ contract MockETHStrategy is ERC20("Mock cEther Strategy", "cEther", 18), ETHStra } function balanceOfUnderlying(address user) external view override returns (uint256) { - return balanceOf[user].fmul(exchangeRate(), 1e18); + return balanceOf[user].mulDivDown(exchangeRate(), 1e18); } /*/////////////////////////////////////////////////////////////// - INTERNAL LOGIC + INTERNAL LOGIC //////////////////////////////////////////////////////////////*/ function exchangeRate() internal view returns (uint256) { @@ -44,11 +44,11 @@ contract MockETHStrategy is ERC20("Mock cEther Strategy", "cEther", 18), ETHStra if (cTokenSupply == 0) return 1e18; - return address(this).balance.fdiv(cTokenSupply, 1e18); + return address(this).balance.divWadDown(cTokenSupply); } /*/////////////////////////////////////////////////////////////// - MOCK FUNCTIONS + MOCK LOGIC //////////////////////////////////////////////////////////////*/ function simulateLoss(uint256 underlyingAmount) external { diff --git a/src/test/utils/DSTestPlus.sol b/src/test/utils/DSTestPlus.sol deleted file mode 100644 index b9dd610..0000000 --- a/src/test/utils/DSTestPlus.sol +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -pragma solidity 0.8.9; - -import {DSTestPlus as DSTest} from "solmate/test/utils/DSTestPlus.sol"; -import {ERC20} from "solmate/tokens/ERC20.sol"; - -import {Vault} from "../../Vault.sol"; - -contract DSTestPlus is DSTest { - function assertERC20Eq(ERC20 erc1, ERC20 erc2) internal { - assertEq(address(erc1), address(erc2)); - } - - function assertVaultEq(Vault va, Vault vb) public { - assertEq(address(va), address(vb)); - } -} diff --git a/whitepaper/Whitepaper.pdf b/whitepaper/Whitepaper.pdf new file mode 100644 index 0000000..e69de29 diff --git a/whitepaper/Whitepaper.tex b/whitepaper/Whitepaper.tex new file mode 100644 index 0000000..e69de29