From 7bff2eed07f6a6001e4f07d31af05cf8d015987f Mon Sep 17 00:00:00 2001 From: Elliot Date: Mon, 9 Jan 2023 15:02:29 -0800 Subject: [PATCH 01/74] PCV Oracle Accounting Comment --- contracts/pcv/PCVGuardian.sol | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/contracts/pcv/PCVGuardian.sol b/contracts/pcv/PCVGuardian.sol index 762f62b62..551e80379 100644 --- a/contracts/pcv/PCVGuardian.sol +++ b/contracts/pcv/PCVGuardian.sol @@ -124,6 +124,19 @@ contract PCVGuardian is IPCVGuardian, CoreRefV2 { } } + // ---------- PCV Guardian State-Changing API ---------- + + // ----------------------------------------------------- + // ------------------- WARNING!!! --------------------- + // ----------------------------------------------------- + // USING THESE FUNCTIONS WILL BREAK ACCOUNTING + // IN THE PCV ORACLE. ONLY USE FUNCTIONS IN AN + // EMERGENCY SITUATION IF WITHDRAWING FROM PCV DEPOSITS. + // ----------------------------------------------------- + // WITHDRAWING FROM A PSM WILL NOT BREAK ACCOUNTING. + // ----------------------------------------------------- + // ----------------------------------------------------- + /// @notice governor-or-guardian-or-pcv-guard method to withdraw funds from a pcv deposit, by calling the withdraw() method on it /// @param pcvDeposit the address of the pcv deposit contract /// @param amount the amount to withdraw From d941e155be771f93ed5da18ae099d74b2809f4b2 Mon Sep 17 00:00:00 2001 From: Elliot Date: Mon, 9 Jan 2023 15:03:03 -0800 Subject: [PATCH 02/74] update time to 2024 for oracle start price --- contracts/deployment/SystemV2.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/deployment/SystemV2.sol b/contracts/deployment/SystemV2.sol index b6cf972ba..64c2f86f0 100644 --- a/contracts/deployment/SystemV2.sol +++ b/contracts/deployment/SystemV2.sol @@ -122,7 +122,7 @@ contract SystemV2 { /// ---------- ORACLE PARAMS ---------- - uint40 public constant VOLT_APR_START_TIME = 1672531200; /// 2023-01-01 + uint40 public constant VOLT_APR_START_TIME = 1704096000; /// 2023-01-01 uint200 public constant VOLT_START_PRICE = 1.05e18; uint16 public constant VOLT_MONTHLY_BASIS_POINTS = 14; From 8948fe9bdeae95c7e840a8ccb7d37768ce1a9e93 Mon Sep 17 00:00:00 2001 From: Elliot Date: Mon, 9 Jan 2023 15:04:50 -0800 Subject: [PATCH 03/74] Liquid PCV Deposit role count and member checks --- .../test/integration/IntegrationTestSystemV2.t.sol | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/contracts/test/integration/IntegrationTestSystemV2.t.sol b/contracts/test/integration/IntegrationTestSystemV2.t.sol index 3354858ae..78b0fc107 100644 --- a/contracts/test/integration/IntegrationTestSystemV2.t.sol +++ b/contracts/test/integration/IntegrationTestSystemV2.t.sol @@ -182,21 +182,13 @@ contract IntegrationTestSystemV2 is Test { ); // LIQUID_PCV_DEPOSIT_ROLE - assertEq(core.getRoleMemberCount(VoltRoles.LIQUID_PCV_DEPOSIT), 4); + assertEq(core.getRoleMemberCount(VoltRoles.LIQUID_PCV_DEPOSIT), 2); assertEq( core.getRoleMember(VoltRoles.LIQUID_PCV_DEPOSIT, 0), - address(systemV2.daipsm()) - ); - assertEq( - core.getRoleMember(VoltRoles.LIQUID_PCV_DEPOSIT, 1), - address(systemV2.usdcpsm()) - ); - assertEq( - core.getRoleMember(VoltRoles.LIQUID_PCV_DEPOSIT, 2), address(systemV2.morphoDaiPCVDeposit()) ); assertEq( - core.getRoleMember(VoltRoles.LIQUID_PCV_DEPOSIT, 3), + core.getRoleMember(VoltRoles.LIQUID_PCV_DEPOSIT, 1), address(systemV2.morphoUsdcPCVDeposit()) ); From b5cadfe7b72e71177bf5a35631bcb166c1d8ad35 Mon Sep 17 00:00:00 2001 From: Elliot Date: Mon, 9 Jan 2023 15:08:54 -0800 Subject: [PATCH 04/74] TODO in deployment file around start time and price --- contracts/deployment/SystemV2.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/deployment/SystemV2.sol b/contracts/deployment/SystemV2.sol index 64c2f86f0..2af4ee418 100644 --- a/contracts/deployment/SystemV2.sol +++ b/contracts/deployment/SystemV2.sol @@ -122,8 +122,8 @@ contract SystemV2 { /// ---------- ORACLE PARAMS ---------- - uint40 public constant VOLT_APR_START_TIME = 1704096000; /// 2023-01-01 - uint200 public constant VOLT_START_PRICE = 1.05e18; + uint40 public constant VOLT_APR_START_TIME = 1704096000; /// 2024-01-01 TODO change this based on when the system starts + uint200 public constant VOLT_START_PRICE = 1.05e18; /// TODO change this based on when the system starts uint16 public constant VOLT_MONTHLY_BASIS_POINTS = 14; function deploy() public { From 0b6e393ec572aafa2119328f863765fabaf00cc3 Mon Sep 17 00:00:00 2001 From: Elliot Date: Mon, 9 Jan 2023 15:13:56 -0800 Subject: [PATCH 05/74] Remove LIQUID_PCV_DEPOSIT role from psm --- contracts/deployment/SystemV2.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/contracts/deployment/SystemV2.sol b/contracts/deployment/SystemV2.sol index 2af4ee418..6d6248b2b 100644 --- a/contracts/deployment/SystemV2.sol +++ b/contracts/deployment/SystemV2.sol @@ -292,8 +292,6 @@ contract SystemV2 { core.createRole(VoltRoles.LIQUID_PCV_DEPOSIT, VoltRoles.GOVERNOR); core.createRole(VoltRoles.ILLIQUID_PCV_DEPOSIT, VoltRoles.GOVERNOR); - core.grantRole(VoltRoles.LIQUID_PCV_DEPOSIT, address(daipsm)); - core.grantRole(VoltRoles.LIQUID_PCV_DEPOSIT, address(usdcpsm)); core.grantRole( VoltRoles.LIQUID_PCV_DEPOSIT, address(morphoDaiPCVDeposit) From c097365503e480229f8ba6bc55d9ce1eaacbd866 Mon Sep 17 00:00:00 2001 From: Elliot Date: Mon, 9 Jan 2023 20:05:33 -0800 Subject: [PATCH 06/74] PCV Deposit V2 --- contracts/pcv/IPCVDepositV2.sol | 2 + contracts/pcv/PCVDepositV2.sol | 128 ++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 contracts/pcv/PCVDepositV2.sol diff --git a/contracts/pcv/IPCVDepositV2.sol b/contracts/pcv/IPCVDepositV2.sol index ce2e144fc..bac589bb7 100644 --- a/contracts/pcv/IPCVDepositV2.sol +++ b/contracts/pcv/IPCVDepositV2.sol @@ -11,4 +11,6 @@ interface IPCVDepositV2 is IPCVDeposit { function harvest() external; function accrue() external returns (uint256); + + function token() external returns (address); } diff --git a/contracts/pcv/PCVDepositV2.sol b/contracts/pcv/PCVDepositV2.sol new file mode 100644 index 000000000..5996cd043 --- /dev/null +++ b/contracts/pcv/PCVDepositV2.sol @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.13; + +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {CoreRefV2} from "../refs/CoreRefV2.sol"; +import {IPCVDepositV2} from "./IPCVDepositV2.sol"; + +/// @title abstract contract for withdrawing ERC-20 tokens using a PCV Controller +/// @author Elliot Friedman +abstract contract PCVDepositV2 is IPCVDepositV2, CoreRefV2 { + using SafeERC20 for IERC20; + + /// ------------------------------------------ + /// ------------- State Variables ------------ + /// ------------------------------------------ + + /// @notice track the last amount of PCV recorded in the contract + /// this is always out of date, except when accrue() is called + /// in the same block or transaction. This means the value is stale + /// most of the time. + uint128 public lastRecordedBalance; + + /// @notice track the last amount of profits earned by the contract + /// this is always out of date, except when accrue() is called + /// in the same block or transaction. This means the value is stale + /// most of the time. + int128 public lastRecordedProfits; + + /// ------------- Internal Helpers ------------- + + /// @notice records how much profit or loss has been accrued + /// since the last call and emits an event with all profit or loss received. + /// Updates the lastRecordedBalance to include all realized profits or losses. + /// @return profit accumulated since last _recordPNL() call. + function _recordPNL() internal returns (int256) { + /// first accrue interest in the underlying venue + _accrue(); + + /// ------ Check ------ + + /// then get the current balance from the market + uint256 currentBalance = balance(); + + /// save gas if contract has no balance + /// if cost basis is 0 and last recorded balance is 0 + /// there is no profit or loss to record and no reason + /// to update lastRecordedBalance + if (currentBalance == 0 && lastRecordedBalance == 0) { + return 0; + } + + /// currentBalance should always be greater than or equal to + /// the deposited amount, except on the same block a deposit occurs, or a loss event in morpho + /// SLOAD + uint128 _lastRecordedBalance = lastRecordedBalance; + + /// Compute profit + int128 profit = int128(int256(currentBalance)) - + int128(_lastRecordedBalance); + + int128 _lastRecordedProfits = lastRecordedProfits + profit; + + /// ------ Effects ------ + + /// SSTORE: record new amounts + lastRecordedProfits = _lastRecordedProfits; + lastRecordedBalance = uint128(currentBalance); + + /// profit is in underlying token + emit Harvest(token(), int256(profit), block.timestamp); + + return profit; + } + + /// @notice helper function to avoid repeated code in withdraw and withdrawAll + /// anytime this function is called it is by an external function in this smart contract + /// with a reentrancy guard. This ensures lastRecordedBalance never desynchronizes. + /// Morpho is assumed to be a loss-less venue. over the course of less than 1 block, + /// it is possible to lose funds. However, after 1 block, deposits are expected to always + /// be in profit at least with current interest rates around 0.8% natively on Compound, + /// ignoring all COMP and Morpho rewards. + /// @param to recipient of withdraw funds + /// @param amount to withdraw + /// @param recordPnl whether or not to record PnL. Set to false in withdrawAll + /// as the function _recordPNL() is already called before _withdraw + function _withdraw( + address to, + uint256 amount, + bool recordPnl + ) private returns (int256 profit) { + /// ------ Effects ------ + + if (recordPnl) { + /// compute profit from interest accrued and emit a Harvest event + profit = _recordPNL(); + } + + /// update last recorded balance amount + /// if more than is owned is withdrawn, this line will revert + /// this line of code is both a check, and an effect + lastRecordedBalance -= uint128(amount); + + /// ------ Interactions ------ + + _withdraw(amount); + IERC20(token()).safeTransfer(to, amount); + + emit Withdrawal(msg.sender, to, amount); + } + + /// ------------- Virtual Functions ------------- + + /// @notice function to get balance in the underlying market. + /// @return current balance of deposit + function balance() public view virtual override returns (uint256); + + /// @dev function to get the underlying token. + function token() public view virtual override returns (address); + + /// @dev function to accrue in the underlying market. + function _accrue() internal virtual; + + /// @dev function to accrue in the underlying market. + function _withdraw(uint256 amount) internal virtual; +} From 5726f39629cb50f8f99bdebdd436cb52e84ee83b Mon Sep 17 00:00:00 2001 From: Elliot Date: Tue, 17 Jan 2023 18:16:16 -0800 Subject: [PATCH 07/74] PCVDeposit V2 and Morpho PCV Deposit refactor --- contracts/deployment/SystemV2.sol | 12 +- .../mock/MockMorphoMaliciousReentrancy.sol | 6 +- contracts/pcv/IPCVDepositV2.sol | 64 +++- contracts/pcv/PCVDepositV2.sol | 164 ++++++++- .../pcv/morpho/MorphoCompoundPCVDeposit.sol | 314 ----------------- contracts/pcv/morpho/MorphoPCVDeposit.sol | 135 ++++++++ contracts/pcv/utils/ERC20Allocator.sol | 4 +- .../IntegrationTestERC20Allocator.t.sol | 22 +- ...egrationTestMorphoCompoundPCVDeposit.t.sol | 12 +- .../integration/IntegrationTestSystemV2.t.sol | 12 +- .../IntegrationTestCompoundPCVRouter.t.sol | 14 +- .../IntegrationTestRateLimiters.sol | 316 ++++++++++++++++++ .../PostProposalCheck.sol | 26 ++ contracts/test/integration/vip/vip14.sol | 14 +- .../InvariantTestMorphoPCVDeposit.t.sol | 11 +- .../pcv/morpho/MorphoCompoundPCVDeposit.t.sol | 24 +- .../test/unit/pcv/utils/ERC20Allocator.t.sol | 26 +- 17 files changed, 757 insertions(+), 419 deletions(-) delete mode 100644 contracts/pcv/morpho/MorphoCompoundPCVDeposit.sol create mode 100644 contracts/pcv/morpho/MorphoPCVDeposit.sol create mode 100644 contracts/test/integration/post-proposal-checks/IntegrationTestRateLimiters.sol create mode 100644 contracts/test/integration/post-proposal-checks/PostProposalCheck.sol diff --git a/contracts/deployment/SystemV2.sol b/contracts/deployment/SystemV2.sol index 6d6248b2b..cd4e5f13a 100644 --- a/contracts/deployment/SystemV2.sol +++ b/contracts/deployment/SystemV2.sol @@ -27,7 +27,7 @@ import {MainnetAddresses} from "../test/integration/fixtures/MainnetAddresses.so import {PegStabilityModule} from "../peg/PegStabilityModule.sol"; import {ConstantPriceOracle} from "../oracle/ConstantPriceOracle.sol"; import {IPCVDeposit, PCVDeposit} from "../pcv/PCVDeposit.sol"; -import {MorphoCompoundPCVDeposit} from "../pcv/morpho/MorphoCompoundPCVDeposit.sol"; +import {MorphoPCVDeposit} from "../pcv/morpho/MorphoPCVDeposit.sol"; import {IVoltMigrator, VoltMigrator} from "../volt/VoltMigrator.sol"; import {IGlobalReentrancyLock, GlobalReentrancyLock} from "../core/GlobalReentrancyLock.sol"; import {IGlobalRateLimitedMinter, GlobalRateLimitedMinter} from "../limiter/GlobalRateLimitedMinter.sol"; @@ -57,8 +57,8 @@ contract SystemV2 { MigratorRouter public migratorRouter; /// PCV Deposits - MorphoCompoundPCVDeposit public morphoDaiPCVDeposit; - MorphoCompoundPCVDeposit public morphoUsdcPCVDeposit; + MorphoPCVDeposit public morphoDaiPCVDeposit; + MorphoPCVDeposit public morphoUsdcPCVDeposit; /// Peg Stability PegStabilityModule public daipsm; @@ -167,18 +167,20 @@ contract SystemV2 { ); /// PCV Deposits - morphoDaiPCVDeposit = new MorphoCompoundPCVDeposit( + morphoDaiPCVDeposit = new MorphoPCVDeposit( address(core), MainnetAddresses.CDAI, address(dai), + MainnetAddresses.COMP, MainnetAddresses.MORPHO, MainnetAddresses.MORPHO_LENS ); - morphoUsdcPCVDeposit = new MorphoCompoundPCVDeposit( + morphoUsdcPCVDeposit = new MorphoPCVDeposit( address(core), MainnetAddresses.CUSDC, address(usdc), + MainnetAddresses.COMP, MainnetAddresses.MORPHO, MainnetAddresses.MORPHO_LENS ); diff --git a/contracts/mock/MockMorphoMaliciousReentrancy.sol b/contracts/mock/MockMorphoMaliciousReentrancy.sol index 8d7efb12f..e7ab6884b 100644 --- a/contracts/mock/MockMorphoMaliciousReentrancy.sol +++ b/contracts/mock/MockMorphoMaliciousReentrancy.sol @@ -1,12 +1,12 @@ pragma solidity 0.8.13; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import {MorphoCompoundPCVDeposit} from "contracts/pcv/morpho/MorphoCompoundPCVDeposit.sol"; +import {MorphoPCVDeposit} from "contracts/pcv/morpho/MorphoPCVDeposit.sol"; contract MockMorphoMaliciousReentrancy { IERC20 public immutable token; mapping(address => uint256) public balances; - MorphoCompoundPCVDeposit public morphoCompoundPCVDeposit; + MorphoPCVDeposit public morphoCompoundPCVDeposit; constructor(IERC20 _token) { token = _token; @@ -17,7 +17,7 @@ contract MockMorphoMaliciousReentrancy { } function setMorphoCompoundPCVDeposit(address deposit) external { - morphoCompoundPCVDeposit = MorphoCompoundPCVDeposit(deposit); + morphoCompoundPCVDeposit = MorphoPCVDeposit(deposit); } function withdraw(address, uint256) external { diff --git a/contracts/pcv/IPCVDepositV2.sol b/contracts/pcv/IPCVDepositV2.sol index bac589bb7..15633e730 100644 --- a/contracts/pcv/IPCVDepositV2.sol +++ b/contracts/pcv/IPCVDepositV2.sol @@ -1,16 +1,70 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.13; -import {IPCVDeposit} from "./IPCVDeposit.sol"; - /// @title PCV V2 Deposit interface /// @author Volt Protocol -interface IPCVDepositV2 is IPCVDeposit { - // ----------- State changing api ----------- +interface IPCVDepositV2 { + // ----------- Events ----------- + + event Deposit(address indexed _from, uint256 _amount); + + event Withdrawal( + address indexed _caller, + address indexed _to, + uint256 _amount + ); + + event WithdrawERC20( + address indexed _caller, + address indexed _token, + address indexed _to, + uint256 _amount + ); + + event WithdrawETH( + address indexed _caller, + address indexed _to, + uint256 _amount + ); + + event Harvest(address indexed _token, int256 _profit, uint256 _timestamp); + + // ----------- PCV Controller only state changing api ----------- + + function withdraw(address to, uint256 amount) external; + function withdrawERC20(address token, address to, uint256 amount) external; + + // ----------- Permissionless State changing api ----------- + + /// @notice deposit ERC-20 tokens to underlying venue + /// non-reentrant to block malicious reentrant state changes + /// to the lastRecordedBalance variable + function deposit() external; + + /// @notice claim COMP rewards for supplying to Morpho. + /// Does not require reentrancy lock as no smart contract state is mutated + /// in this function. function harvest() external; + /// @notice function that emits an event tracking profits and losses + /// since the last contract interaction + /// then writes the current amount of PCV tracked in this contract + /// to lastRecordedBalance + /// @return the amount deposited after adding accrued interest or realizing losses function accrue() external returns (uint256); - function token() external returns (address); + // ----------- Getters ----------- + + /// @notice gets the effective balance of "balanceReportedIn" token if the deposit were fully withdrawn + function balance() external view returns (uint256); + + /// @notice gets the token address in which this deposit returns its balance + function balanceReportedIn() external view returns (address); + + /// @notice address of underlying token + function token() external view returns (address); + + /// @notice address of reward token + function rewardToken() external view returns (address); } diff --git a/contracts/pcv/PCVDepositV2.sol b/contracts/pcv/PCVDepositV2.sol index 5996cd043..5b3ab019a 100644 --- a/contracts/pcv/PCVDepositV2.sol +++ b/contracts/pcv/PCVDepositV2.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.13; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -12,6 +13,13 @@ import {IPCVDepositV2} from "./IPCVDepositV2.sol"; /// @author Elliot Friedman abstract contract PCVDepositV2 is IPCVDepositV2, CoreRefV2 { using SafeERC20 for IERC20; + using SafeCast for *; + + /// @notice reference to underlying token + address public immutable override rewardToken; + + /// @notice reference to underlying token + address public immutable override token; /// ------------------------------------------ /// ------------- State Variables ------------ @@ -29,6 +37,132 @@ abstract contract PCVDepositV2 is IPCVDepositV2, CoreRefV2 { /// most of the time. int128 public lastRecordedProfits; + constructor(address _token, address _rewardToken) { + token = _token; + rewardToken = _rewardToken; + } + + /// ------------------------------------------ + /// ----------- Permissionless API ----------- + /// ------------------------------------------ + + /// @notice deposit ERC-20 tokens to Morpho-Compound + /// non-reentrant to block malicious reentrant state changes + /// to the lastRecordedBalance variable + function deposit() public whenNotPaused globalLock(2) { + /// ------ Check ------ + + uint256 amount = IERC20(token).balanceOf(address(this)); + if (amount == 0) { + /// no op to prevent revert on empty deposit + return; + } + + int256 startingRecordedBalance = lastRecordedBalance.toInt256(); + + /// ------ Effects ------ + + /// compute profit from interest accrued and emit an event + /// if any profits or losses are realized + int256 profit = _recordPNL(); + + /// increment tracked recorded amount + /// this will be off by a hair, after a single block + /// negative delta turns to positive delta (assuming no loss). + lastRecordedBalance += uint128(amount); + + /// ------ Interactions ------ + + /// approval and deposit into underlying venue + _supply(amount); + + /// ------ Update Internal Accounting ------ + + int256 endingRecordedBalance = balance().toInt256(); + + _liquidPcvOracleHook( + endingRecordedBalance - startingRecordedBalance, + profit + ); + + emit Deposit(msg.sender, amount); + } + + /// @notice claim COMP rewards for supplying to Morpho. + /// Does not require reentrancy lock as no smart contract state is mutated + /// in this function. + function harvest() external globalLock(2) { + uint256 claimedAmount = _claim(); + + emit Harvest(rewardToken, int256(claimedAmount), block.timestamp); + } + + /// @notice function that emits an event tracking profits and losses + /// since the last contract interaction + /// then writes the current amount of PCV tracked in this contract + /// to lastRecordedBalance + /// @return the amount deposited after adding accrued interest or realizing losses + function accrue() external globalLock(2) whenNotPaused returns (uint256) { + int256 profit = _recordPNL(); /// update deposit amount and fire harvest event + + /// if any amount of PCV is withdrawn and no gains, delta is negative + _liquidPcvOracleHook(profit, profit); + + return lastRecordedBalance; /// return updated pcv amount + } + + /// ------------------------------------------ + /// ------------ Permissioned API ------------ + /// ------------------------------------------ + + /// @notice withdraw tokens from the PCV allocation + /// non-reentrant as state changes and external calls are made + /// @param to the address PCV will be sent to + /// @param amount of tokens withdrawn + function withdraw( + address to, + uint256 amount + ) external onlyPCVController globalLock(2) { + int256 profit = _withdraw(to, amount, true); + + /// if any amount of PCV is withdrawn and no gains, delta is negative + _liquidPcvOracleHook(-(amount.toInt256()) + profit, profit); + } + + /// @notice withdraw all tokens from Morpho + /// non-reentrant as state changes and external calls are made + /// @param to the address PCV will be sent to + function withdrawAll(address to) external onlyPCVController globalLock(2) { + /// compute profit from interest accrued and emit an event + int256 profit = _recordPNL(); + + int256 recordedBalance = lastRecordedBalance.toInt256(); + + /// withdraw last recorded amount as this was updated in record pnl + _withdraw(to, lastRecordedBalance, false); + + /// all PCV withdrawn, send call in with amount withdrawn negative if any amount is withdrawn + _liquidPcvOracleHook(-recordedBalance, profit); + } + + /// @notice withdraw ERC20 from the contract + /// @param tokenAddress address of the ERC20 to send + /// @param to address destination of the ERC20 + /// @param amount quantity of ERC20 to send + /// Calling this function will lead to incorrect + /// accounting in a PCV deposit that tracks + /// profits and or last recorded balance. + /// If a deposit records PNL, only use this + /// function in an emergency. + function withdrawERC20( + address tokenAddress, + address to, + uint256 amount + ) public virtual override onlyPCVController { + IERC20(tokenAddress).safeTransfer(to, amount); + emit WithdrawERC20(msg.sender, tokenAddress, to, amount); + } + /// ------------- Internal Helpers ------------- /// @notice records how much profit or loss has been accrued @@ -37,7 +171,7 @@ abstract contract PCVDepositV2 is IPCVDepositV2, CoreRefV2 { /// @return profit accumulated since last _recordPNL() call. function _recordPNL() internal returns (int256) { /// first accrue interest in the underlying venue - _accrue(); + _accrueUnderlying(); /// ------ Check ------ @@ -70,12 +204,12 @@ abstract contract PCVDepositV2 is IPCVDepositV2, CoreRefV2 { lastRecordedBalance = uint128(currentBalance); /// profit is in underlying token - emit Harvest(token(), int256(profit), block.timestamp); + emit Harvest(token, int256(profit), block.timestamp); return profit; } - /// @notice helper function to avoid repeated code in withdraw and withdrawAll + /// @notice helper avoid repeated code in withdraw and withdrawAll /// anytime this function is called it is by an external function in this smart contract /// with a reentrancy guard. This ensures lastRecordedBalance never desynchronizes. /// Morpho is assumed to be a loss-less venue. over the course of less than 1 block, @@ -105,24 +239,30 @@ abstract contract PCVDepositV2 is IPCVDepositV2, CoreRefV2 { /// ------ Interactions ------ - _withdraw(amount); - IERC20(token()).safeTransfer(to, amount); + /// remove funds from underlying venue + _withdrawUnderlyingVenue(amount); + /// transfer funds to recipient + IERC20(token).safeTransfer(to, amount); emit Withdrawal(msg.sender, to, amount); } /// ------------- Virtual Functions ------------- - /// @notice function to get balance in the underlying market. + /// @notice get balance in the underlying market. /// @return current balance of deposit function balance() public view virtual override returns (uint256); - /// @dev function to get the underlying token. - function token() public view virtual override returns (address); + /// @dev accrue interest in the underlying market. + function _accrueUnderlying() internal virtual; + + /// @dev withdraw from the underlying market. + function _withdrawUnderlyingVenue(uint256 amount) internal virtual; - /// @dev function to accrue in the underlying market. - function _accrue() internal virtual; + /// @dev deposit in the underlying market. + function _supply(uint256 amount) internal virtual; - /// @dev function to accrue in the underlying market. - function _withdraw(uint256 amount) internal virtual; + /// @dev claim rewards from the underlying market. + /// returns amount of reward tokens claimed + function _claim() internal virtual returns (uint256); } diff --git a/contracts/pcv/morpho/MorphoCompoundPCVDeposit.sol b/contracts/pcv/morpho/MorphoCompoundPCVDeposit.sol deleted file mode 100644 index db6e9a345..000000000 --- a/contracts/pcv/morpho/MorphoCompoundPCVDeposit.sol +++ /dev/null @@ -1,314 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity =0.8.13; - -import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -import {ILens} from "./ILens.sol"; -import {ICToken} from "./ICompound.sol"; -import {IMorpho} from "./IMorpho.sol"; -import {CoreRefV2} from "../../refs/CoreRefV2.sol"; -import {Constants} from "../../Constants.sol"; -import {PCVDeposit} from "../PCVDeposit.sol"; - -/// @notice PCV Deposit for Morpho-Compound V2. -/// Implements the PCV Deposit interface to deposit and withdraw funds in Morpho -/// Liquidity profile of Morpho for this deposit is fully liquid for USDC and DAI -/// because the incentivized rates are higher than the P2P rate. -/// Only for depositing USDC and DAI. USDT is not in scope. -/// @dev approves the Morpho Deposit to spend this PCV deposit's token, -/// and then calls supply on Morpho, which pulls the underlying token to Morpho, -/// drawing down on the approved amount to be spent, -/// and then giving this PCV Deposit credits on Morpho in exchange for the underlying -/// @dev PCV Guardian functions withdrawERC20ToSafeAddress and withdrawAllERC20ToSafeAddress -/// will not work with removing Morpho Tokens on the Morpho PCV Deposit because Morpho -/// has no concept of mTokens. This means if the contract is paused, or an issue is -/// surfaced in Morpho and liquidity is locked, Volt will need to rely on social -/// coordination with the Morpho team to recover funds. -/// @dev Depositing and withdrawing in a single block will cause a very small loss -/// of funds, less than a pip. The way to not realize this loss is by depositing and -/// then withdrawing at least 1 block later. That way, interest accrues. -/// This is not a Morpho specific issue. Compound rounds in the protocol's favor. -/// The issue is caused by constraints inherent to solidity and the EVM. -/// There are no floating point numbers, this means there is precision loss, -/// and protocol engineers are forced to choose who to round in favor of. -/// Engineers must round in favor of the protocol to avoid deposits of 0 giving -/// the user a balance. -contract MorphoCompoundPCVDeposit is PCVDeposit { - using SafeERC20 for IERC20; - using SafeCast for *; - - /// ------------------------------------------ - /// ---------- Immutables/Constant ----------- - /// ------------------------------------------ - - /// @notice reference to the COMP governance token - /// used for recording COMP rewards type in Harvest event - address public constant COMP = 0xc00e94Cb662C3520282E6f5717214004A7f26888; - - /// @notice reference to the lens contract for morpho-compound v2 - address public immutable lens; - - /// @notice reference to the morpho-compound v2 market - address public immutable morpho; - - /// @notice reference to underlying token - address public immutable token; - - /// @notice cToken in compound this deposit tracks - /// used to inform morpho about the desired market to supply liquidity - address public immutable cToken; - - /// ------------------------------------------ - /// ------------- State Variables ------------- - /// ------------------------------------------ - - /// @notice track the last amount of PCV recorded in the contract - /// this is always out of date, except when accrue() is called - /// in the same block or transaction. This means the value is stale - /// most of the time. - uint128 public lastRecordedBalance; - - /// @notice track the last amount of profits earned by the contract - /// this is always out of date, except when accrue() is called - /// in the same block or transaction. This means the value is stale - /// most of the time. - int128 public lastRecordedProfits; - - /// @param _core reference to the core contract - /// @param _cToken cToken this deposit references - /// @param _underlying Token denomination of this deposit - /// @param _morpho reference to the morpho-compound v2 market - /// @param _lens reference to the morpho-compound v2 lens - constructor( - address _core, - address _cToken, - address _underlying, - address _morpho, - address _lens - ) CoreRefV2(_core) { - if (_underlying != address(Constants.WETH)) { - require( - ICToken(_cToken).underlying() == _underlying, - "MorphoCompoundPCVDeposit: Underlying mismatch" - ); - } - cToken = _cToken; - token = _underlying; - morpho = _morpho; - lens = _lens; - } - - /// ------------------------------------------ - /// ------------------ Views ----------------- - /// ------------------------------------------ - - /// @notice Returns the distribution of assets supplied by this contract through Morpho-Compound. - /// @return sum of suppliedP2P and suppliedOnPool for the given CToken - function balance() public view override returns (uint256) { - (, , uint256 totalSupplied) = ILens(lens).getCurrentSupplyBalanceInOf( - cToken, - address(this) - ); - - return totalSupplied; - } - - /// @notice returns the underlying token of this deposit - function balanceReportedIn() external view returns (address) { - return token; - } - - /// ------------------------------------------ - /// ----------- Permissionless API ----------- - /// ------------------------------------------ - - /// @notice deposit ERC-20 tokens to Morpho-Compound - /// non-reentrant to block malicious reentrant state changes - /// to the lastRecordedBalance variable - function deposit() public whenNotPaused globalLock(2) { - /// ------ Check ------ - - uint256 amount = IERC20(token).balanceOf(address(this)); - if (amount == 0) { - /// no op to prevent revert on empty deposit - return; - } - - int256 startingRecordedBalance = lastRecordedBalance.toInt256(); - - /// ------ Effects ------ - - /// compute profit from interest accrued and emit an event - /// if any profits or losses are realized - int256 profit = _recordPNL(); - - /// increment tracked recorded amount - /// this will be off by a hair, after a single block - /// negative delta turns to positive delta (assuming no loss). - lastRecordedBalance += uint128(amount); - - /// ------ Interactions ------ - - IERC20(token).approve(address(morpho), amount); - IMorpho(morpho).supply( - cToken, /// cToken to supply liquidity to - address(this), /// the address of the user you want to supply on behalf of - amount - ); - - int256 endingRecordedBalance = balance().toInt256(); - - _liquidPcvOracleHook( - endingRecordedBalance - startingRecordedBalance, - profit - ); - - emit Deposit(msg.sender, amount); - } - - /// @notice claim COMP rewards for supplying to Morpho. - /// Does not require reentrancy lock as no smart contract state is mutated - /// in this function. - function harvest() external globalLock(2) { - address[] memory cTokens = new address[](1); - cTokens[0] = cToken; - - /// set swap comp to morpho flag false to claim comp rewards - uint256 claimedAmount = IMorpho(morpho).claimRewards(cTokens, false); - - emit Harvest(COMP, int256(claimedAmount), block.timestamp); - } - - /// @notice function that emits an event tracking profits and losses - /// since the last contract interaction - /// then writes the current amount of PCV tracked in this contract - /// to lastRecordedBalance - /// @return the amount deposited after adding accrued interest or realizing losses - function accrue() external globalLock(2) whenNotPaused returns (uint256) { - int256 profit = _recordPNL(); /// update deposit amount and fire harvest event - - /// if any amount of PCV is withdrawn and no gains, delta is negative - _liquidPcvOracleHook(profit, profit); - - return lastRecordedBalance; /// return updated pcv amount - } - - /// ------------------------------------------ - /// ------------ Permissioned API ------------ - /// ------------------------------------------ - - /// @notice withdraw tokens from the PCV allocation - /// non-reentrant as state changes and external calls are made - /// @param to the address PCV will be sent to - /// @param amount of tokens withdrawn - function withdraw( - address to, - uint256 amount - ) external onlyPCVController globalLock(2) { - int256 profit = _withdraw(to, amount, true); - - /// if any amount of PCV is withdrawn and no gains, delta is negative - _liquidPcvOracleHook(-(amount.toInt256()) + profit, profit); - } - - /// @notice withdraw all tokens from Morpho - /// non-reentrant as state changes and external calls are made - /// @param to the address PCV will be sent to - function withdrawAll(address to) external onlyPCVController globalLock(2) { - /// compute profit from interest accrued and emit an event - int256 profit = _recordPNL(); - - int256 recordedBalance = lastRecordedBalance.toInt256(); - - /// withdraw last recorded amount as this was updated in record pnl - _withdraw(to, lastRecordedBalance, false); - - /// all PCV withdrawn, send call in with amount withdrawn negative if any amount is withdrawn - _liquidPcvOracleHook(-recordedBalance, profit); - } - - /// ------------------------------------------ - /// ------------- Helper Methods ------------- - /// ------------------------------------------ - - /// @notice helper function to avoid repeated code in withdraw and withdrawAll - /// anytime this function is called it is by an external function in this smart contract - /// with a reentrancy guard. This ensures lastRecordedBalance never desynchronizes. - /// Morpho is assumed to be a loss-less venue. over the course of less than 1 block, - /// it is possible to lose funds. However, after 1 block, deposits are expected to always - /// be in profit at least with current interest rates around 0.8% natively on Compound, - /// ignoring all COMP and Morpho rewards. - /// @param to recipient of withdraw funds - /// @param amount to withdraw - /// @param recordPnl whether or not to record PnL. Set to false in withdrawAll - /// as the function _recordPNL() is already called before _withdraw - function _withdraw( - address to, - uint256 amount, - bool recordPnl - ) private returns (int256 profit) { - /// ------ Effects ------ - - if (recordPnl) { - /// compute profit from interest accrued and emit a Harvest event - profit = _recordPNL(); - } - - /// update last recorded balance amount - /// if more than is owned is withdrawn, this line will revert - /// this line of code is both a check, and an effect - lastRecordedBalance -= uint128(amount); - - /// ------ Interactions ------ - - IMorpho(morpho).withdraw(cToken, amount); - IERC20(token).safeTransfer(to, amount); - - emit Withdrawal(msg.sender, to, amount); - } - - /// @notice records how much profit or loss has been accrued - /// since the last call and emits an event with all profit or loss received. - /// Updates the lastRecordedBalance to include all realized profits or losses. - /// @return profit accumulated since last _recordPNL() call. - function _recordPNL() private returns (int256) { - /// first accrue interest in Compound and Morpho - IMorpho(morpho).updateP2PIndexes(cToken); - - /// ------ Check ------ - - /// then get the current balance from the market - uint256 currentBalance = balance(); - - /// save gas if contract has no balance - /// if cost basis is 0 and last recorded balance is 0 - /// there is no profit or loss to record and no reason - /// to update lastRecordedBalance - if (currentBalance == 0 && lastRecordedBalance == 0) { - return 0; - } - - /// currentBalance should always be greater than or equal to - /// the deposited amount, except on the same block a deposit occurs, or a loss event in morpho - /// SLOAD - uint128 _lastRecordedBalance = lastRecordedBalance; - int128 _lastRecordedProfits = lastRecordedProfits; - - /// Compute profit - int128 profit = int128(int256(currentBalance)) - - int128(_lastRecordedBalance); - - /// ------ Effects ------ - - /// SSTORE: record new amounts - lastRecordedProfits = _lastRecordedProfits + profit; - lastRecordedBalance = uint128(currentBalance); - - /// profit is in underlying token - emit Harvest(token, int256(profit), block.timestamp); - - return profit; - } -} diff --git a/contracts/pcv/morpho/MorphoPCVDeposit.sol b/contracts/pcv/morpho/MorphoPCVDeposit.sol new file mode 100644 index 000000000..06c45ccac --- /dev/null +++ b/contracts/pcv/morpho/MorphoPCVDeposit.sol @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity =0.8.13; + +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {ILens} from "./ILens.sol"; +import {ICToken} from "./ICompound.sol"; +import {IMorpho} from "./IMorpho.sol"; +import {CoreRefV2} from "../../refs/CoreRefV2.sol"; +import {Constants} from "../../Constants.sol"; +import {PCVDepositV2} from "../PCVDepositV2.sol"; + +/// @notice PCV Deposit for Morpho-Compound V2. +/// Implements the PCV Deposit interface to deposit and withdraw funds in Morpho +/// Liquidity profile of Morpho for this deposit is fully liquid for USDC and DAI +/// because the incentivized rates are higher than the P2P rate. +/// Only for depositing USDC and DAI. USDT is not in scope. +/// @dev approves the Morpho Deposit to spend this PCV deposit's token, +/// and then calls supply on Morpho, which pulls the underlying token to Morpho, +/// drawing down on the approved amount to be spent, +/// and then giving this PCV Deposit credits on Morpho in exchange for the underlying +/// @dev PCV Guardian functions withdrawERC20ToSafeAddress and withdrawAllERC20ToSafeAddress +/// will not work with removing Morpho Tokens on the Morpho PCV Deposit because Morpho +/// has no concept of mTokens. This means if the contract is paused, or an issue is +/// surfaced in Morpho and liquidity is locked, Volt will need to rely on social +/// coordination with the Morpho team to recover funds. +/// @dev Depositing and withdrawing in a single block will cause a very small loss +/// of funds, less than a pip. The way to not realize this loss is by depositing and +/// then withdrawing at least 1 block later. That way, interest accrues. +/// This is not a Morpho specific issue. Compound rounds in the protocol's favor. +/// The issue is caused by constraints inherent to solidity and the EVM. +/// There are no floating point numbers, this means there is precision loss, +/// and protocol engineers are forced to choose who to round in favor of. +/// Engineers must round in favor of the protocol to avoid deposits of 0 giving +/// the user a balance. +contract MorphoPCVDeposit is PCVDepositV2 { + using SafeERC20 for IERC20; + using SafeCast for *; + + /// ------------------------------------------ + /// ---------- Immutables/Constant ----------- + /// ------------------------------------------ + + /// @notice reference to the lens contract for morpho-compound v2 + address public immutable lens; + + /// @notice reference to the morpho-compound v2 market + address public immutable morpho; + + /// @notice cToken in compound this deposit tracks + /// used to inform morpho about the desired market to supply liquidity + address public immutable cToken; + + /// @param _core reference to the core contract + /// @param _cToken cToken this deposit references + /// @param _underlying Token denomination of this deposit + /// @param _rewardToken Reward token denomination of this deposit + /// @param _morpho reference to the morpho-compound v2 market + /// @param _lens reference to the morpho-compound v2 lens + constructor( + address _core, + address _cToken, + address _underlying, + address _rewardToken, + address _morpho, + address _lens + ) PCVDepositV2(_underlying, _rewardToken) CoreRefV2(_core) { + if (_underlying != address(Constants.WETH)) { + require( + ICToken(_cToken).underlying() == _underlying, + "MorphoPCVDeposit: Underlying mismatch" + ); + } + cToken = _cToken; + token = _underlying; + morpho = _morpho; + lens = _lens; + } + + /// ------------------------------------------ + /// ------------------ Views ----------------- + /// ------------------------------------------ + + /// @notice Returns the distribution of assets supplied by this contract through Morpho-Compound. + /// @return sum of suppliedP2P and suppliedOnPool for the given CToken + function balance() public view override returns (uint256) { + (, , uint256 totalSupplied) = ILens(lens).getCurrentSupplyBalanceInOf( + cToken, + address(this) + ); + + return totalSupplied; + } + + /// @notice returns the underlying token of this deposit + function balanceReportedIn() external view returns (address) { + return token; + } + + /// ------------------------------------------ + /// ------------- Helper Methods ------------- + /// ------------------------------------------ + + /// @notice accrue interest in the underlying morpho venue + function _accrueUnderlying() internal override { + /// accrue interest in Morpho + IMorpho(morpho).updateP2PIndexes(cToken); + } + + /// @dev withdraw from the underlying morpho market. + function _withdrawUnderlyingVenue(uint256 amount) internal override { + IMorpho(morpho).withdraw(cToken, amount); + } + + /// @dev deposit in the underlying morpho market. + function _supply(uint256 amount) internal override { + IERC20(token).approve(address(morpho), amount); + IMorpho(morpho).supply( + cToken, /// cToken to supply liquidity to + address(this), /// the address of the user you want to supply on behalf of + amount + ); + } + + /// @dev claim rewards from the underlying Compound market. + /// returns amount of reward tokens claimed + function _claim() internal override returns (uint256) { + address[] memory cTokens = new address[](1); + cTokens[0] = cToken; + + return IMorpho(morpho).claimRewards(cTokens, false); /// bool set false to receive COMP + } +} diff --git a/contracts/pcv/utils/ERC20Allocator.sol b/contracts/pcv/utils/ERC20Allocator.sol index 300bbb434..ae5ea5af2 100644 --- a/contracts/pcv/utils/ERC20Allocator.sol +++ b/contracts/pcv/utils/ERC20Allocator.sol @@ -300,7 +300,7 @@ contract ERC20Allocator is IERC20Allocator, CoreRefV2 { /// reverts if not drip eligbile function getDripDetails( address psm, - PCVDeposit pcvDeposit + address pcvDeposit ) public view returns (uint256 amountToDrip, uint256 adjustedAmountToDrip) { PSMInfo memory toDrip = allPSMs[psm]; @@ -325,7 +325,7 @@ contract ERC20Allocator is IERC20Allocator, CoreRefV2 { /// amountToDrip = 1,000e6 amountToDrip = Math.min( - Math.min(targetBalanceDelta, pcvDeposit.balance()), + Math.min(targetBalanceDelta, PCVDeposit(pcvDeposit).balance()), /// adjust for decimals here as buffer is 1e18 scaled, /// and if token is not scaled by 1e18, then this amountToDrip could be over the buffer /// because buffer is 1e18 adjusted, and decimals normalizer is used to adjust up to the buffer diff --git a/contracts/test/integration/IntegrationTestERC20Allocator.t.sol b/contracts/test/integration/IntegrationTestERC20Allocator.t.sol index 9841e1ffc..32c2d9e78 100644 --- a/contracts/test/integration/IntegrationTestERC20Allocator.t.sol +++ b/contracts/test/integration/IntegrationTestERC20Allocator.t.sol @@ -17,7 +17,7 @@ import {ERC20Allocator} from "../../pcv/utils/ERC20Allocator.sol"; import {MainnetAddresses} from "./fixtures/MainnetAddresses.sol"; import {TimelockSimulation} from "./utils/TimelockSimulation.sol"; import {PegStabilityModule} from "../../peg/PegStabilityModule.sol"; -import {MorphoCompoundPCVDeposit} from "../../pcv/morpho/MorphoCompoundPCVDeposit.sol"; +import {MorphoPCVDeposit} from "../../pcv/morpho/MorphoPCVDeposit.sol"; contract IntegrationTestERC20Allocator is DSTest { using SafeCast for *; @@ -26,14 +26,10 @@ contract IntegrationTestERC20Allocator is DSTest { PCVGuardian private immutable mainnetPCVGuardian = PCVGuardian(MainnetAddresses.PCV_GUARDIAN); - MorphoCompoundPCVDeposit private daiDeposit = - MorphoCompoundPCVDeposit( - MainnetAddresses.MORPHO_COMPOUND_DAI_PCV_DEPOSIT - ); - MorphoCompoundPCVDeposit private usdcDeposit = - MorphoCompoundPCVDeposit( - MainnetAddresses.MORPHO_COMPOUND_USDC_PCV_DEPOSIT - ); + MorphoPCVDeposit private daiDeposit = + MorphoPCVDeposit(MainnetAddresses.MORPHO_COMPOUND_DAI_PCV_DEPOSIT); + MorphoPCVDeposit private usdcDeposit = + MorphoPCVDeposit(MainnetAddresses.MORPHO_COMPOUND_USDC_PCV_DEPOSIT); PCVGuardian private immutable pcvGuardian = PCVGuardian(MainnetAddresses.PCV_GUARDIAN); @@ -116,7 +112,7 @@ contract IntegrationTestERC20Allocator is DSTest { daiDeposit.deposit(); (uint256 amountToDrip, uint256 adjustedAmountToDrip) = allocator - .getDripDetails(address(daiPSM), daiDeposit); + .getDripDetails(address(daiPSM), address(daiDeposit)); assertTrue(allocator.checkDripCondition(address(daiDeposit))); assertTrue(!allocator.checkSkimCondition(address(daiDeposit))); @@ -141,7 +137,7 @@ contract IntegrationTestERC20Allocator is DSTest { usdcDeposit.deposit(); /// deposit so it will be counted in balance (uint256 amountToDrip, uint256 adjustedAmountToDrip) = allocator - .getDripDetails(address(usdcPSM), usdcDeposit); + .getDripDetails(address(usdcPSM), address(usdcDeposit)); assertTrue(allocator.checkDripCondition(address(usdcDeposit))); assertTrue(allocator.checkActionAllowed(address(usdcDeposit))); @@ -231,7 +227,7 @@ contract IntegrationTestERC20Allocator is DSTest { daiDeposit.deposit(); (uint256 amountToDrip, uint256 adjustedAmountToDrip) = allocator - .getDripDetails(address(daiPSM), daiDeposit); + .getDripDetails(address(daiPSM), address(daiDeposit)); assertTrue(allocator.checkDripCondition(address(daiDeposit))); assertTrue(!allocator.checkSkimCondition(address(daiDeposit))); @@ -258,7 +254,7 @@ contract IntegrationTestERC20Allocator is DSTest { usdcDeposit.deposit(); /// deposit so it will be counted in balance (uint256 amountToDrip, uint256 adjustedAmountToDrip) = allocator - .getDripDetails(address(usdcPSM), usdcDeposit); + .getDripDetails(address(usdcPSM), address(usdcDeposit)); assertTrue(allocator.checkDripCondition(address(usdcDeposit))); assertTrue(allocator.checkActionAllowed(address(usdcDeposit))); diff --git a/contracts/test/integration/IntegrationTestMorphoCompoundPCVDeposit.t.sol b/contracts/test/integration/IntegrationTestMorphoCompoundPCVDeposit.t.sol index 97fc6ba40..7a7156eb2 100644 --- a/contracts/test/integration/IntegrationTestMorphoCompoundPCVDeposit.t.sol +++ b/contracts/test/integration/IntegrationTestMorphoCompoundPCVDeposit.t.sol @@ -16,7 +16,7 @@ import {SystemEntry} from "../../entry/SystemEntry.sol"; import {MainnetAddresses} from "./fixtures/MainnetAddresses.sol"; import {PegStabilityModule} from "../../peg/PegStabilityModule.sol"; import {ERC20CompoundPCVDeposit} from "../../pcv/compound/ERC20CompoundPCVDeposit.sol"; -import {MorphoCompoundPCVDeposit} from "../../pcv/morpho/MorphoCompoundPCVDeposit.sol"; +import {MorphoPCVDeposit} from "../../pcv/morpho/MorphoPCVDeposit.sol"; import {TestAddresses as addresses} from "../unit/utils/TestAddresses.sol"; import {IGlobalReentrancyLock, GlobalReentrancyLock} from "../../core/GlobalReentrancyLock.sol"; @@ -28,8 +28,8 @@ contract IntegrationTestMorphoCompoundPCVDeposit is DSTest { CoreV2 private core; SystemEntry public entry; GlobalReentrancyLock private lock; - MorphoCompoundPCVDeposit private daiDeposit; - MorphoCompoundPCVDeposit private usdcDeposit; + MorphoPCVDeposit private daiDeposit; + MorphoPCVDeposit private usdcDeposit; PCVGuardian private pcvGuardian; @@ -51,18 +51,20 @@ contract IntegrationTestMorphoCompoundPCVDeposit is DSTest { function setUp() public { core = getCoreV2(); lock = new GlobalReentrancyLock(address(core)); - daiDeposit = new MorphoCompoundPCVDeposit( + daiDeposit = new MorphoPCVDeposit( address(core), MainnetAddresses.CDAI, MainnetAddresses.DAI, + MainnetAddresses.COMP, MainnetAddresses.MORPHO, MainnetAddresses.MORPHO_LENS ); - usdcDeposit = new MorphoCompoundPCVDeposit( + usdcDeposit = new MorphoPCVDeposit( address(core), MainnetAddresses.CUSDC, MainnetAddresses.USDC, + MainnetAddresses.COMP, MainnetAddresses.MORPHO, MainnetAddresses.MORPHO_LENS ); diff --git a/contracts/test/integration/IntegrationTestSystemV2.t.sol b/contracts/test/integration/IntegrationTestSystemV2.t.sol index 78b0fc107..ee1bd6137 100644 --- a/contracts/test/integration/IntegrationTestSystemV2.t.sol +++ b/contracts/test/integration/IntegrationTestSystemV2.t.sol @@ -35,8 +35,8 @@ contract IntegrationTestSystemV2 is Test { PCVOracle pcvOracle; TimelockController timelockController; PCVGuardian pcvGuardian; - MorphoCompoundPCVDeposit morphoDaiPCVDeposit; - MorphoCompoundPCVDeposit morphoUsdcPCVDeposit; + MorphoPCVDeposit morphoDaiPCVDeposit; + MorphoPCVDeposit morphoUsdcPCVDeposit; PCVRouter pcvRouter; SystemEntry systemEntry; VoltMigrator voltMigrator; @@ -837,10 +837,10 @@ contract IntegrationTestSystemV2 is Test { Internal helper function, migrate the PCV from current system to V2 system */ function _migratePcv() internal returns (uint256) { - MorphoCompoundPCVDeposit oldMorphoDaiPCVDeposit = MorphoCompoundPCVDeposit( - MainnetAddresses.MORPHO_COMPOUND_DAI_PCV_DEPOSIT - ); - /*MorphoCompoundPCVDeposit oldMorphoUsdcPCVDeposit = MorphoCompoundPCVDeposit( + MorphoPCVDeposit oldMorphoDaiPCVDeposit = MorphoPCVDeposit( + MainnetAddresses.MORPHO_COMPOUND_DAI_PCV_DEPOSIT + ); + /*MorphoPCVDeposit oldMorphoUsdcPCVDeposit = MorphoPCVDeposit( MainnetAddresses.MORPHO_COMPOUND_USDC_PCV_DEPOSIT );*/ PCVGuardian oldPCVGuardian = PCVGuardian(MainnetAddresses.PCV_GUARDIAN); diff --git a/contracts/test/integration/compound/IntegrationTestCompoundPCVRouter.t.sol b/contracts/test/integration/compound/IntegrationTestCompoundPCVRouter.t.sol index 43d414257..6bfc32360 100644 --- a/contracts/test/integration/compound/IntegrationTestCompoundPCVRouter.t.sol +++ b/contracts/test/integration/compound/IntegrationTestCompoundPCVRouter.t.sol @@ -15,7 +15,7 @@ import {PCVGuardian} from "../../../pcv/PCVGuardian.sol"; import {MainnetAddresses} from "../fixtures/MainnetAddresses.sol"; import {ArbitrumAddresses} from "../fixtures/ArbitrumAddresses.sol"; import {CompoundPCVRouter} from "../../../pcv/compound/CompoundPCVRouter.sol"; -import {MorphoCompoundPCVDeposit} from "../../../pcv/morpho/MorphoCompoundPCVDeposit.sol"; +import {MorphoPCVDeposit} from "../../../pcv/morpho/MorphoPCVDeposit.sol"; contract IntegrationTestCompoundPCVRouter is DSTest { using SafeCast for *; @@ -36,14 +36,10 @@ contract IntegrationTestCompoundPCVRouter is DSTest { CompoundPCVRouter private compoundRouter = CompoundPCVRouter(MainnetAddresses.MORPHO_COMPOUND_PCV_ROUTER); - MorphoCompoundPCVDeposit private daiDeposit = - MorphoCompoundPCVDeposit( - MainnetAddresses.MORPHO_COMPOUND_DAI_PCV_DEPOSIT - ); - MorphoCompoundPCVDeposit private usdcDeposit = - MorphoCompoundPCVDeposit( - MainnetAddresses.MORPHO_COMPOUND_USDC_PCV_DEPOSIT - ); + MorphoPCVDeposit private daiDeposit = + MorphoPCVDeposit(MainnetAddresses.MORPHO_COMPOUND_DAI_PCV_DEPOSIT); + MorphoPCVDeposit private usdcDeposit = + MorphoPCVDeposit(MainnetAddresses.MORPHO_COMPOUND_USDC_PCV_DEPOSIT); address public immutable pcvGuard = MainnetAddresses.EOA_1; PCVGuardian public immutable pcvGuardian = diff --git a/contracts/test/integration/post-proposal-checks/IntegrationTestRateLimiters.sol b/contracts/test/integration/post-proposal-checks/IntegrationTestRateLimiters.sol new file mode 100644 index 000000000..25ff2dfbe --- /dev/null +++ b/contracts/test/integration/post-proposal-checks/IntegrationTestRateLimiters.sol @@ -0,0 +1,316 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.13; + +import {PostProposalCheck} from "./PostProposalCheck.sol"; + +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; + +import {VoltV2} from "../../../volt/VoltV2.sol"; +import {CoreV2} from "../../../core/CoreV2.sol"; +import {PCVOracle} from "../../../oracle/PCVOracle.sol"; +import {PCVDeposit} from "../../../pcv/PCVDeposit.sol"; +import {SystemEntry} from "../../../entry/SystemEntry.sol"; +import {ERC20Allocator} from "../../../pcv/utils/ERC20Allocator.sol"; +import {NonCustodialPSM} from "../../../peg/NonCustodialPSM.sol"; +import {GlobalRateLimitedMinter} from "../../../limiter/GlobalRateLimitedMinter.sol"; +import {GlobalSystemExitRateLimiter} from "../../../limiter/GlobalSystemExitRateLimiter.sol"; +import {PegStabilityModule} from "../../../peg/PegStabilityModule.sol"; + +contract IntegrationTestRateLimiters is PostProposalCheck { + using SafeCast for *; + + address public constant user = 0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7; + uint256 public snapshotAfterMints; + + CoreV2 private core; + VoltV2 private volt; + SystemEntry private systemEntry; + ERC20Allocator private allocator; + PegStabilityModule private usdcpsm; + PegStabilityModule private daipsm; + NonCustodialPSM private usdcncpsm; + NonCustodialPSM private daincpsm; + IERC20 private dai; + IERC20 private usdc; + GlobalRateLimitedMinter private grlm; + GlobalSystemExitRateLimiter private gserl; + PCVOracle private pcvOracle; + address private morphoUsdcPCVDeposit; + address private morphoDaiPCVDeposit; + + function setUp() public override { + super.setUp(); + + core = CoreV2(addresses.mainnet("CORE")); + volt = VoltV2(addresses.mainnet("VOLT")); + systemEntry = SystemEntry(addresses.mainnet("SYSTEM_ENTRY")); + allocator = ERC20Allocator(addresses.mainnet("PSM_ALLOCATOR")); + usdcpsm = PegStabilityModule(addresses.mainnet("PSM_USDC")); + daipsm = PegStabilityModule(addresses.mainnet("PSM_DAI")); + usdcncpsm = NonCustodialPSM(addresses.mainnet("PSM_NONCUSTODIAL_USDC")); + daincpsm = NonCustodialPSM(addresses.mainnet("PSM_NONCUSTODIAL_DAI")); + dai = IERC20(addresses.mainnet("DAI")); + usdc = IERC20(addresses.mainnet("USDC")); + grlm = GlobalRateLimitedMinter( + addresses.mainnet("GLOBAL_RATE_LIMITED_MINTER") + ); + gserl = GlobalSystemExitRateLimiter( + addresses.mainnet("GLOBAL_SYSTEM_EXIT_RATE_LIMITER") + ); + pcvOracle = PCVOracle(addresses.mainnet("PCV_ORACLE")); + morphoUsdcPCVDeposit = addresses.mainnet("PCV_DEPOSIT_MORPHO_USDC"); + morphoDaiPCVDeposit = addresses.mainnet("PCV_DEPOSIT_MORPHO_DAI"); + } + + /* + Flow of the first user that mints VOLT in the new system. + Performs checks on the global rate limits, and accounting + in the new system's PCV Oracle. + */ + function testUserPSMMint() public { + // read initial buffer left + uint256 bufferCap = grlm.bufferCap(); + uint256 initialBuffer = grlm.buffer(); + + // number of moved funds for tests + uint256 amount = initialBuffer / 2; + (, uint256 daiPSMTargetBalance, ) = allocator.allPSMs(address(daipsm)); + (, uint256 usdcPSMTargetBalance, ) = allocator.allPSMs( + address(usdcpsm) + ); + + // read initial pcv + (uint256 startLiquidPcv, , ) = pcvOracle.getTotalPcv(); + // read initial psm balances + uint256 startPsmDaiBalance = dai.balanceOf(address(daipsm)); + uint256 startPsmUsdcBalance = usdc.balanceOf(address(usdcpsm)); + + // user performs the first mint with DAI + vm.startPrank(user); + dai.approve(address(daipsm), amount); + daipsm.mint(user, amount, 0); + vm.stopPrank(); + + (, uint256 adjustedSkimAmount) = allocator.getSkimDetails( + address(morphoDaiPCVDeposit) + ); + uint256 gserlStartingBuffer = gserl.buffer(); + + // buffer has been used + uint256 voltReceived1 = volt.balanceOf(user); + assertEq(grlm.buffer(), initialBuffer - voltReceived1); + + allocator.skim(morphoDaiPCVDeposit); + + /// assert replenish occurred if not maxed out + assertEq( + Math.min( + gserlStartingBuffer + adjustedSkimAmount, + gserlStartingBuffer + ), + gserl.buffer() + ); + + // after first mint, pcv increased by amount + (uint256 liquidPcv2, , ) = pcvOracle.getTotalPcv(); + assertApproxEq( + liquidPcv2.toInt256(), + (startLiquidPcv + startPsmDaiBalance + amount - daiPSMTargetBalance) + .toInt256(), + 0 + ); + + // user performs the second mint wit USDC + vm.startPrank(user); + usdc.approve(address(usdcpsm), amount / 1e12); + usdcpsm.mint(user, amount / 1e12, 0); + vm.stopPrank(); + uint256 voltReceived2 = volt.balanceOf(user) - voltReceived1; + + (, adjustedSkimAmount) = allocator.getSkimDetails( + address(morphoUsdcPCVDeposit) + ); + gserlStartingBuffer = gserl.buffer(); + + // buffer has been used + assertEq(grlm.buffer(), initialBuffer - voltReceived1 - voltReceived2); + + allocator.skim(morphoUsdcPCVDeposit); + { + // after second mint, pcv is = 2 * amount + (uint256 liquidPcv3, , ) = pcvOracle.getTotalPcv(); + assertApproxEq( + liquidPcv3.toInt256(), + (liquidPcv2 + + startPsmUsdcBalance * + 1e12 + + amount - + usdcPSMTargetBalance * + 1e12).toInt256(), + 0 + ); + } + + /// assert replenish occurred if not maxed out + assertEq( + Math.min( + gserlStartingBuffer + adjustedSkimAmount, + gserlStartingBuffer + ), + gserl.buffer() + ); + + snapshotAfterMints = vm.snapshot(); + + vm.prank(address(core)); + grlm.setRateLimitPerSecond(5.787e18); + + // buffer replenishes over time + vm.warp(block.timestamp + 3 days); + + // above limit rate reverts + vm.startPrank(user); + dai.approve(address(daipsm), bufferCap * 2); + vm.expectRevert("RateLimited: rate limit hit"); + daipsm.mint(user, bufferCap * 2, 0); + vm.stopPrank(); + } + + function testRedeemsDaiPsm(uint88 voltAmount) public { + testUserPSMMint(); + + vm.revertTo(snapshotAfterMints); + vm.assume(voltAmount <= volt.balanceOf(user)); + + uint256 daiAmountOut = daipsm.getRedeemAmountOut(voltAmount); + deal(address(dai), address(daipsm), daiAmountOut); + + vm.startPrank(user); + + uint256 startingBuffer = grlm.buffer(); + uint256 startingDaiBalance = dai.balanceOf(user); + + volt.approve(address(daipsm), voltAmount); + daipsm.redeem(user, voltAmount, daiAmountOut); + + uint256 endingBuffer = grlm.buffer(); + uint256 endingDaiBalance = dai.balanceOf(user); + + assertEq(endingDaiBalance - startingDaiBalance, daiAmountOut); + assertEq(endingBuffer - startingBuffer, voltAmount); + + vm.stopPrank(); + + uint256 startingExitBuffer = gserl.buffer(); + (, uint256 expectedBufferDepletion) = allocator.getDripDetails( + address(daipsm), + address(morphoDaiPCVDeposit) + ); + allocator.drip(address(morphoDaiPCVDeposit)); + assertEq(startingExitBuffer - expectedBufferDepletion, gserl.buffer()); + } + + function testRedeemsDaiNcPsm(uint80 voltRedeemAmount) public { + vm.assume(voltRedeemAmount >= 1e18); + vm.assume(voltRedeemAmount <= 400_000e18); + testUserPSMMint(); + + vm.revertTo(snapshotAfterMints); + + uint256 daiAmountOut = daincpsm.getRedeemAmountOut(voltRedeemAmount); + deal(address(dai), morphoDaiPCVDeposit, daiAmountOut * 2); + systemEntry.deposit(morphoDaiPCVDeposit); + + vm.startPrank(user); + + uint256 startingBuffer = grlm.buffer(); + uint256 startingExitBuffer = gserl.buffer(); + uint256 startingDaiBalance = dai.balanceOf(user); + + volt.approve(address(daincpsm), voltRedeemAmount); + daincpsm.redeem(user, voltRedeemAmount, daiAmountOut); + + uint256 endingBuffer = grlm.buffer(); + uint256 endingExitBuffer = gserl.buffer(); + uint256 endingDaiBalance = dai.balanceOf(user); + + assertEq(endingBuffer - startingBuffer, voltRedeemAmount); /// grlm buffer replenished + assertEq(endingDaiBalance - startingDaiBalance, daiAmountOut); + assertEq(startingExitBuffer - endingExitBuffer, daiAmountOut); /// exit buffer depleted + + vm.stopPrank(); + } + + function testRedeemsUsdcNcPsm(uint80 voltRedeemAmount) public { + vm.assume(voltRedeemAmount >= 1e18); + vm.assume(voltRedeemAmount <= 400_000e18); + testUserPSMMint(); + + vm.revertTo(snapshotAfterMints); + + uint256 usdcAmountOut = usdcncpsm.getRedeemAmountOut(voltRedeemAmount); + deal(address(usdc), morphoUsdcPCVDeposit, usdcAmountOut * 2); + systemEntry.deposit(morphoUsdcPCVDeposit); + + vm.startPrank(user); + + uint256 startingBuffer = grlm.buffer(); + uint256 startingExitBuffer = gserl.buffer(); + uint256 startingUsdcBalance = usdc.balanceOf(user); + + volt.approve(address(usdcncpsm), voltRedeemAmount); + usdcncpsm.redeem(user, voltRedeemAmount, usdcAmountOut); + + uint256 endingBuffer = grlm.buffer(); + uint256 endingExitBuffer = gserl.buffer(); + uint256 endingUsdcBalance = usdc.balanceOf(user); + + assertEq(endingBuffer - startingBuffer, voltRedeemAmount); /// buffer replenished + assertEq(endingUsdcBalance - startingUsdcBalance, usdcAmountOut); + assertEq(startingExitBuffer - endingExitBuffer, usdcAmountOut * 1e12); /// ensure buffer adjusted up 12 decimals, buffer depleted + + vm.stopPrank(); + } + + function testRedeemsUsdcPsm(uint80 voltRedeemAmount) public { + vm.assume(voltRedeemAmount >= 1e18); + vm.assume(voltRedeemAmount <= 475_000e18); + testUserPSMMint(); + + vm.revertTo(snapshotAfterMints); + + uint256 usdcAmountOut = usdcpsm.getRedeemAmountOut(voltRedeemAmount); + deal(address(usdc), address(usdcpsm), usdcAmountOut); + + uint256 startingBuffer = grlm.buffer(); + uint256 startingExitBuffer = gserl.buffer(); + uint256 startingUsdcBalance = usdc.balanceOf(user); + + vm.startPrank(user); + volt.approve(address(usdcpsm), voltRedeemAmount); + usdcpsm.redeem(user, voltRedeemAmount, usdcAmountOut); + vm.stopPrank(); + + (, uint256 expectedBufferDepletion) = allocator.getDripDetails( + address(usdcpsm), + address(morphoUsdcPCVDeposit) + ); + allocator.drip(address(morphoUsdcPCVDeposit)); + uint256 endingExitBuffer = gserl.buffer(); + + assertEq( + startingExitBuffer - endingExitBuffer, + expectedBufferDepletion + ); /// ensure buffer adjusted up 12 decimals, buffer depleted + + assertEq(expectedBufferDepletion, gserl.bufferCap() - gserl.buffer()); + + uint256 endingBuffer = grlm.buffer(); + uint256 endingUsdcBalance = usdc.balanceOf(user); + + assertEq(endingBuffer - startingBuffer, voltRedeemAmount); + assertEq(endingUsdcBalance - startingUsdcBalance, usdcAmountOut); + } +} diff --git a/contracts/test/integration/post-proposal-checks/PostProposalCheck.sol b/contracts/test/integration/post-proposal-checks/PostProposalCheck.sol new file mode 100644 index 000000000..3188709d9 --- /dev/null +++ b/contracts/test/integration/post-proposal-checks/PostProposalCheck.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.13; + +import {Test} from "../../../../forge-std/src/Test.sol"; + +import {Addresses} from "../../proposals/Addresses.sol"; +import {TestProposals} from "../../proposals/TestProposals.sol"; + +contract PostProposalCheck is Test { + Addresses addresses; + uint256 preProposalsSnapshot; + uint256 postProposalsSnapshot; + + function setUp() public virtual { + preProposalsSnapshot = vm.snapshot(); + + // Run all pending proposals before doing e2e tests + TestProposals proposals = new TestProposals(); + proposals.setUp(); + proposals.setDebug(false); + proposals.testProposals(); + addresses = proposals.addresses(); + + postProposalsSnapshot = vm.snapshot(); + } +} diff --git a/contracts/test/integration/vip/vip14.sol b/contracts/test/integration/vip/vip14.sol index 466710a8e..fad947ef0 100644 --- a/contracts/test/integration/vip/vip14.sol +++ b/contracts/test/integration/vip/vip14.sol @@ -17,7 +17,7 @@ import {ERC20Allocator} from "../../../pcv/utils/ERC20Allocator.sol"; import {MainnetAddresses} from "../fixtures/MainnetAddresses.sol"; import {CompoundPCVRouter} from "../../../pcv/compound/CompoundPCVRouter.sol"; import {ITimelockSimulation} from "../utils/ITimelockSimulation.sol"; -import {MorphoCompoundPCVDeposit} from "../../../pcv/morpho/MorphoCompoundPCVDeposit.sol"; +import {MorphoPCVDeposit} from "../../../pcv/morpho/MorphoPCVDeposit.sol"; /// Deployment Steps /// 1. deploy morpho dai deposit @@ -54,14 +54,10 @@ contract vip14 is DSTest, IVIP { CompoundPCVRouter public router = CompoundPCVRouter(MainnetAddresses.MORPHO_COMPOUND_PCV_ROUTER); - MorphoCompoundPCVDeposit public daiDeposit = - MorphoCompoundPCVDeposit( - MainnetAddresses.MORPHO_COMPOUND_DAI_PCV_DEPOSIT - ); - MorphoCompoundPCVDeposit public usdcDeposit = - MorphoCompoundPCVDeposit( - MainnetAddresses.MORPHO_COMPOUND_USDC_PCV_DEPOSIT - ); + MorphoPCVDeposit public daiDeposit = + MorphoPCVDeposit(MainnetAddresses.MORPHO_COMPOUND_DAI_PCV_DEPOSIT); + MorphoPCVDeposit public usdcDeposit = + MorphoPCVDeposit(MainnetAddresses.MORPHO_COMPOUND_USDC_PCV_DEPOSIT); PCVGuardian public immutable pcvGuardian = PCVGuardian(MainnetAddresses.PCV_GUARDIAN); diff --git a/contracts/test/invariant/InvariantTestMorphoPCVDeposit.t.sol b/contracts/test/invariant/InvariantTestMorphoPCVDeposit.t.sol index 26a5f85d6..55edf0833 100644 --- a/contracts/test/invariant/InvariantTestMorphoPCVDeposit.t.sol +++ b/contracts/test/invariant/InvariantTestMorphoPCVDeposit.t.sol @@ -15,7 +15,7 @@ import {IPCVOracle} from "../../oracle/IPCVOracle.sol"; import {SystemEntry} from "../../entry/SystemEntry.sol"; import {MockPCVOracle} from "../../mock/MockPCVOracle.sol"; import {DSInvariantTest} from "../unit/utils/DSInvariantTest.sol"; -import {MorphoCompoundPCVDeposit} from "../../pcv/morpho/MorphoCompoundPCVDeposit.sol"; +import {MorphoPCVDeposit} from "../../pcv/morpho/MorphoPCVDeposit.sol"; import {TestAddresses as addresses} from "../unit/utils/TestAddresses.sol"; import {IGlobalReentrancyLock, GlobalReentrancyLock} from "../../core/GlobalReentrancyLock.sol"; @@ -34,7 +34,7 @@ contract InvariantTestMorphoCompoundPCVDeposit is DSTest, DSInvariantTest { MockPCVOracle public pcvOracle; IGlobalReentrancyLock private lock; MorphoPCVDepositTest public morphoTest; - MorphoCompoundPCVDeposit public morphoDeposit; + MorphoPCVDeposit public morphoDeposit; Vm private vm = Vm(HEVM_ADDRESS); @@ -43,10 +43,11 @@ contract InvariantTestMorphoCompoundPCVDeposit is DSTest, DSInvariantTest { token = new MockERC20(); pcvOracle = new MockPCVOracle(); morpho = new MockMorpho(IERC20(address(token))); - morphoDeposit = new MorphoCompoundPCVDeposit( + morphoDeposit = new MorphoPCVDeposit( address(core), address(morpho), address(token), + address(0), /// no need for reward token address(morpho), address(morpho) ); @@ -123,10 +124,10 @@ contract MorphoPCVDepositTest is DSTest { MockMorpho public morpho; SystemEntry public entry; PCVGuardian public pcvGuardian; - MorphoCompoundPCVDeposit public morphoDeposit; + MorphoPCVDeposit public morphoDeposit; constructor( - MorphoCompoundPCVDeposit _morphoDeposit, + MorphoPCVDeposit _morphoDeposit, MockERC20 _token, MockMorpho _morpho, SystemEntry _entry, diff --git a/contracts/test/unit/pcv/morpho/MorphoCompoundPCVDeposit.t.sol b/contracts/test/unit/pcv/morpho/MorphoCompoundPCVDeposit.t.sol index 2902de500..423c614ec 100644 --- a/contracts/test/unit/pcv/morpho/MorphoCompoundPCVDeposit.t.sol +++ b/contracts/test/unit/pcv/morpho/MorphoCompoundPCVDeposit.t.sol @@ -14,7 +14,7 @@ import {PCVGuardian} from "../../../../pcv/PCVGuardian.sol"; import {getCoreV2} from "./../../utils/Fixtures.sol"; import {SystemEntry} from "../../../../entry/SystemEntry.sol"; import {MockERC20, IERC20} from "../../../../mock/MockERC20.sol"; -import {MorphoCompoundPCVDeposit} from "../../../../pcv/morpho/MorphoCompoundPCVDeposit.sol"; +import {MorphoPCVDeposit} from "../../../../pcv/morpho/MorphoPCVDeposit.sol"; import {TestAddresses as addresses} from "../../utils/TestAddresses.sol"; import {MockMorphoMaliciousReentrancy} from "../../../../mock/MockMorphoMaliciousReentrancy.sol"; import {IGlobalReentrancyLock, GlobalReentrancyLock} from "../../../../core/GlobalReentrancyLock.sol"; @@ -36,7 +36,7 @@ contract UnitTestMorphoCompoundPCVDeposit is DSTest { SystemEntry public entry; MockMorpho private morpho; PCVGuardian private pcvGuardian; - MorphoCompoundPCVDeposit private morphoDeposit; + MorphoPCVDeposit private morphoDeposit; MockMorphoMaliciousReentrancy private maliciousMorpho; Vm public constant vm = Vm(HEVM_ADDRESS); @@ -59,10 +59,11 @@ contract UnitTestMorphoCompoundPCVDeposit is DSTest { IERC20(address(token)) ); - morphoDeposit = new MorphoCompoundPCVDeposit( + morphoDeposit = new MorphoPCVDeposit( address(core), address(morpho), address(token), + address(0), address(morpho), address(morpho) ); @@ -103,11 +104,12 @@ contract UnitTestMorphoCompoundPCVDeposit is DSTest { function testUnderlyingMismatchConstructionFails() public { MockCToken cToken = new MockCToken(address(1)); - vm.expectRevert("MorphoCompoundPCVDeposit: Underlying mismatch"); - new MorphoCompoundPCVDeposit( + vm.expectRevert("MorphoPCVDeposit: Underlying mismatch"); + new MorphoPCVDeposit( address(core), address(cToken), address(token), + address(0), address(morpho), address(morpho) ); @@ -253,8 +255,7 @@ contract UnitTestMorphoCompoundPCVDeposit is DSTest { token.mint(address(morphoDeposit), amount); entry.deposit(address(morphoDeposit)); - MorphoCompoundPCVDeposit.Call[] - memory calls = new MorphoCompoundPCVDeposit.Call[](1); + MorphoPCVDeposit.Call[] memory calls = new MorphoPCVDeposit.Call[](1); calls[0].callData = abi.encodeWithSignature( "withdraw(address,uint256)", address(this), @@ -273,8 +274,7 @@ contract UnitTestMorphoCompoundPCVDeposit is DSTest { vm.assume(amount != 0); token.mint(address(morphoDeposit), amount); - MorphoCompoundPCVDeposit.Call[] - memory calls = new MorphoCompoundPCVDeposit.Call[](2); + MorphoPCVDeposit.Call[] memory calls = new MorphoPCVDeposit.Call[](2); calls[0].callData = abi.encodeWithSignature( "approve(address,uint256)", address(morphoDeposit.morpho()), @@ -320,8 +320,7 @@ contract UnitTestMorphoCompoundPCVDeposit is DSTest { //// access controls function testEmergencyActionFailsNonGovernor() public { - MorphoCompoundPCVDeposit.Call[] - memory calls = new MorphoCompoundPCVDeposit.Call[](1); + MorphoPCVDeposit.Call[] memory calls = new MorphoPCVDeposit.Call[](1); calls[0].callData = abi.encodeWithSignature( "withdraw(address,uint256)", address(this), @@ -346,10 +345,11 @@ contract UnitTestMorphoCompoundPCVDeposit is DSTest { //// reentrancy function _reentrantSetup() private { - morphoDeposit = new MorphoCompoundPCVDeposit( + morphoDeposit = new MorphoPCVDeposit( address(core), address(maliciousMorpho), /// cToken is not used in mock morpho deposit address(token), + address(0), address(maliciousMorpho), address(maliciousMorpho) ); diff --git a/contracts/test/unit/pcv/utils/ERC20Allocator.t.sol b/contracts/test/unit/pcv/utils/ERC20Allocator.t.sol index 520313854..53c053aac 100644 --- a/contracts/test/unit/pcv/utils/ERC20Allocator.t.sol +++ b/contracts/test/unit/pcv/utils/ERC20Allocator.t.sol @@ -258,10 +258,7 @@ contract UnitTestERC20Allocator is DSTest { assertEq(gserl.buffer(), targetBalance / 2); (uint256 amountToDrip, uint256 adjustedAmountToDrip) = allocator - .getDripDetails( - address(newPsm), - PCVDeposit(address(newPcvDeposit)) - ); + .getDripDetails(address(newPsm), address(newPcvDeposit)); allocator.drip(address(newPcvDeposit)); @@ -310,10 +307,7 @@ contract UnitTestERC20Allocator is DSTest { assertEq(gserl.buffer(), targetBalance / 2); (uint256 amountToDrip, uint256 adjustedAmountToDrip) = allocator - .getDripDetails( - address(newPsm), - PCVDeposit(address(newPcvDeposit)) - ); + .getDripDetails(address(newPsm), address(newPcvDeposit)); allocator.drip(address(newPcvDeposit)); @@ -701,17 +695,14 @@ contract UnitTestERC20Allocator is DSTest { ( uint256 psmAmountToDrip, uint256 psmAdjustedAmountToDrip - ) = allocator.getDripDetails( - address(psm), - PCVDeposit(address(pcvDeposit)) - ); + ) = allocator.getDripDetails(address(psm), address(pcvDeposit)); ( uint256 newPsmAmountToDrip, uint256 newPsmAdjustedAmountToDrip ) = allocator.getDripDetails( address(newPsm), - PCVDeposit(address(newPcvDeposit)) + address(newPcvDeposit) ); /// drips are 0 because pcv deposits are not funded @@ -730,17 +721,14 @@ contract UnitTestERC20Allocator is DSTest { ( uint256 psmAmountToDrip, uint256 psmAdjustedAmountToDrip - ) = allocator.getDripDetails( - address(psm), - PCVDeposit(address(pcvDeposit)) - ); + ) = allocator.getDripDetails(address(psm), address(pcvDeposit)); ( uint256 newPsmAmountToDrip, uint256 newPsmAdjustedAmountToDrip ) = allocator.getDripDetails( address(newPsm), - PCVDeposit(address(newPcvDeposit)) + address(newPcvDeposit) ); assertEq(psmAmountToDrip, targetBalance); @@ -881,7 +869,7 @@ contract UnitTestERC20Allocator is DSTest { uint256 bufferStart = gserl.buffer(); (uint256 amountToDrip, uint256 adjustedAmountToDrip) = allocator - .getDripDetails(address(psm), PCVDeposit(address(pcvDeposit))); + .getDripDetails(address(psm), address(pcvDeposit)); /// this has to be true assertTrue(allocator.checkDripCondition(address(pcvDeposit))); From 0edc79a9758d34fd64b627116a8f26ef387bd51d Mon Sep 17 00:00:00 2001 From: Elliot Date: Tue, 17 Jan 2023 18:28:10 -0800 Subject: [PATCH 08/74] Update deployment script with new Morpho PCV Deposit --- contracts/pcv/morpho/MorphoPCVDeposit.sol | 2 +- contracts/pcv/utils/ERC20Allocator.sol | 2 +- .../IntegrationTestProposalPSMOracle.sol | 1 - contracts/test/proposals/vips/vip16.sol | 34 ++++++++++--------- contracts/test/unit/system/System.t.sol | 5 +-- 5 files changed, 23 insertions(+), 21 deletions(-) diff --git a/contracts/pcv/morpho/MorphoPCVDeposit.sol b/contracts/pcv/morpho/MorphoPCVDeposit.sol index 06c45ccac..879ce7f15 100644 --- a/contracts/pcv/morpho/MorphoPCVDeposit.sol +++ b/contracts/pcv/morpho/MorphoPCVDeposit.sol @@ -73,8 +73,8 @@ contract MorphoPCVDeposit is PCVDepositV2 { "MorphoPCVDeposit: Underlying mismatch" ); } + cToken = _cToken; - token = _underlying; morpho = _morpho; lens = _lens; } diff --git a/contracts/pcv/utils/ERC20Allocator.sol b/contracts/pcv/utils/ERC20Allocator.sol index 28e305441..3bde151ea 100644 --- a/contracts/pcv/utils/ERC20Allocator.sol +++ b/contracts/pcv/utils/ERC20Allocator.sol @@ -217,7 +217,7 @@ contract ERC20Allocator is IERC20Allocator, CoreRefV2 { (uint256 amountToDrip, uint256 adjustedAmountToDrip) = getDripDetails( psm, - pcvDeposit + address(pcvDeposit) ); /// Effects diff --git a/contracts/test/integration/proposal-checks/IntegrationTestProposalPSMOracle.sol b/contracts/test/integration/proposal-checks/IntegrationTestProposalPSMOracle.sol index 7db83f47f..d189a7413 100644 --- a/contracts/test/integration/proposal-checks/IntegrationTestProposalPSMOracle.sol +++ b/contracts/test/integration/proposal-checks/IntegrationTestProposalPSMOracle.sol @@ -84,7 +84,6 @@ contract IntegrationTestProposalPSMOracle is Test { IERC20 volt = IERC20(addresses.mainnet("VOLT")); IERC20 token = IERC20(addresses.mainnet("USDC")); uint256 amountTokens = 100e6; - uint256 time = block.timestamp; // Read pre-proposal VOLT minted for a known amount of USDC deal(address(token), address(this), amountTokens); diff --git a/contracts/test/proposals/vips/vip16.sol b/contracts/test/proposals/vips/vip16.sol index 1a5529f2f..f74c896a9 100644 --- a/contracts/test/proposals/vips/vip16.sol +++ b/contracts/test/proposals/vips/vip16.sol @@ -28,7 +28,7 @@ import {PegStabilityModule} from "../../../peg/PegStabilityModule.sol"; import {IPegStabilityModule} from "../../../peg/IPegStabilityModule.sol"; import {ConstantPriceOracle} from "../../../oracle/ConstantPriceOracle.sol"; import {IPCVDeposit, PCVDeposit} from "../../../pcv/PCVDeposit.sol"; -import {MorphoCompoundPCVDeposit} from "../../../pcv/morpho/MorphoCompoundPCVDeposit.sol"; +import {MorphoPCVDeposit} from "../../../pcv/morpho/MorphoPCVDeposit.sol"; import {IVoltMigrator, VoltMigrator} from "../../../volt/VoltMigrator.sol"; import {IGlobalReentrancyLock, GlobalReentrancyLock} from "../../../core/GlobalReentrancyLock.sol"; import {IGlobalRateLimitedMinter, GlobalRateLimitedMinter} from "../../../limiter/GlobalRateLimitedMinter.sol"; @@ -161,21 +161,23 @@ contract vip16 is Proposal { /// PCV Deposits { - MorphoCompoundPCVDeposit morphoDaiPCVDeposit = new MorphoCompoundPCVDeposit( - addresses.mainnet("CORE"), - addresses.mainnet("CDAI"), - addresses.mainnet("DAI"), - addresses.mainnet("MORPHO"), - addresses.mainnet("MORPHO_LENS") - ); - - MorphoCompoundPCVDeposit morphoUsdcPCVDeposit = new MorphoCompoundPCVDeposit( - addresses.mainnet("CORE"), - addresses.mainnet("CUSDC"), - addresses.mainnet("USDC"), - addresses.mainnet("MORPHO"), - addresses.mainnet("MORPHO_LENS") - ); + MorphoPCVDeposit morphoDaiPCVDeposit = new MorphoPCVDeposit( + addresses.mainnet("CORE"), + addresses.mainnet("CDAI"), + addresses.mainnet("DAI"), + addresses.mainnet("COMP"), + addresses.mainnet("MORPHO"), + addresses.mainnet("MORPHO_LENS") + ); + + MorphoPCVDeposit morphoUsdcPCVDeposit = new MorphoPCVDeposit( + addresses.mainnet("CORE"), + addresses.mainnet("CUSDC"), + addresses.mainnet("USDC"), + addresses.mainnet("COMP"), + addresses.mainnet("MORPHO"), + addresses.mainnet("MORPHO_LENS") + ); addresses.addMainnet( "PCV_DEPOSIT_MORPHO_DAI", diff --git a/contracts/test/unit/system/System.t.sol b/contracts/test/unit/system/System.t.sol index 3a5df10e7..23607c366 100644 --- a/contracts/test/unit/system/System.t.sol +++ b/contracts/test/unit/system/System.t.sol @@ -22,7 +22,7 @@ import {VoltSystemOracle} from "../../../oracle/VoltSystemOracle.sol"; import {CompoundPCVRouter} from "../../../pcv/compound/CompoundPCVRouter.sol"; import {PegStabilityModule} from "../../../peg/PegStabilityModule.sol"; import {IScalingPriceOracle} from "../../../oracle/IScalingPriceOracle.sol"; -import {MorphoCompoundPCVDeposit} from "../../../pcv/morpho/MorphoCompoundPCVDeposit.sol"; +import {MorphoPCVDeposit} from "../../../pcv/morpho/MorphoPCVDeposit.sol"; import {TestAddresses as addresses} from "../utils/TestAddresses.sol"; import {getCoreV2, getVoltAddresses, VoltAddresses} from "./../utils/Fixtures.sol"; import {IGlobalReentrancyLock, GlobalReentrancyLock} from "../../../core/GlobalReentrancyLock.sol"; @@ -628,10 +628,11 @@ contract SystemUnitTest is Test { bytes4(keccak256("supply(address,address,uint256)")) ); - MorphoCompoundPCVDeposit deposit = new MorphoCompoundPCVDeposit( + MorphoPCVDeposit deposit = new MorphoPCVDeposit( address(core), address(mock), address(usdc), + address(0), address(mock), address(mock) ); From 230221e07572d8fa2e4a8b6115b579e1e0c05872 Mon Sep 17 00:00:00 2001 From: Elliot Date: Tue, 17 Jan 2023 19:28:03 -0800 Subject: [PATCH 09/74] remove system v2 --- contracts/deployment/SystemV2.sol | 390 ----- .../integration/IntegrationTestSystemV2.t.sol | 1390 ----------------- 2 files changed, 1780 deletions(-) delete mode 100644 contracts/deployment/SystemV2.sol delete mode 100644 contracts/test/integration/IntegrationTestSystemV2.t.sol diff --git a/contracts/deployment/SystemV2.sol b/contracts/deployment/SystemV2.sol deleted file mode 100644 index cd4e5f13a..000000000 --- a/contracts/deployment/SystemV2.sol +++ /dev/null @@ -1,390 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.13; - -import {TimelockController} from "@openzeppelin/contracts/governance/TimelockController.sol"; -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -import {Test} from "../../forge-std/src/Test.sol"; -import {IVolt} from "../volt/IVolt.sol"; -import {CoreV2} from "../core/CoreV2.sol"; -import {VoltV2} from "../volt/VoltV2.sol"; -import {VoltRoles} from "../core/VoltRoles.sol"; -import {MockERC20} from "../mock/MockERC20.sol"; -import {PCVRouter} from "../pcv/PCVRouter.sol"; -import {Deviation} from "../utils/Deviation.sol"; -import {PCVOracle} from "../oracle/PCVOracle.sol"; -import {IPCVOracle} from "../oracle/IPCVOracle.sol"; -import {PCVGuardian} from "../pcv/PCVGuardian.sol"; -import {SystemEntry} from "../entry/SystemEntry.sol"; -import {MockCoreRefV2} from "../mock/MockCoreRefV2.sol"; -import {MigratorRouter} from "../pcv/MigratorRouter.sol"; -import {ERC20Allocator} from "../pcv/utils/ERC20Allocator.sol"; -import {NonCustodialPSM} from "../peg/NonCustodialPSM.sol"; -import {MakerPCVSwapper} from "../pcv/maker/MakerPCVSwapper.sol"; -import {VoltSystemOracle} from "../oracle/VoltSystemOracle.sol"; -import {MainnetAddresses} from "../test/integration/fixtures/MainnetAddresses.sol"; -import {PegStabilityModule} from "../peg/PegStabilityModule.sol"; -import {ConstantPriceOracle} from "../oracle/ConstantPriceOracle.sol"; -import {IPCVDeposit, PCVDeposit} from "../pcv/PCVDeposit.sol"; -import {MorphoPCVDeposit} from "../pcv/morpho/MorphoPCVDeposit.sol"; -import {IVoltMigrator, VoltMigrator} from "../volt/VoltMigrator.sol"; -import {IGlobalReentrancyLock, GlobalReentrancyLock} from "../core/GlobalReentrancyLock.sol"; -import {IGlobalRateLimitedMinter, GlobalRateLimitedMinter} from "../limiter/GlobalRateLimitedMinter.sol"; -import {IGlobalSystemExitRateLimiter, GlobalSystemExitRateLimiter} from "../limiter/GlobalSystemExitRateLimiter.sol"; - -contract SystemV2 { - using SafeCast for *; - - /// External - IERC20 public usdc = IERC20(MainnetAddresses.USDC); - IERC20 public dai = IERC20(MainnetAddresses.DAI); - IERC20 public voltV1 = IERC20(0x559eBC30b0E58a45Cc9fF573f77EF1e5eb1b3E18); - - /// Core - VoltV2 public volt; - CoreV2 public core; - TimelockController public timelockController; - GlobalReentrancyLock public lock; - GlobalRateLimitedMinter public grlm; - GlobalSystemExitRateLimiter public gserl; - - /// VOLT rate - VoltSystemOracle public vso; - - /// Volt Migration - VoltMigrator public voltMigrator; - MigratorRouter public migratorRouter; - - /// PCV Deposits - MorphoPCVDeposit public morphoDaiPCVDeposit; - MorphoPCVDeposit public morphoUsdcPCVDeposit; - - /// Peg Stability - PegStabilityModule public daipsm; - PegStabilityModule public usdcpsm; - NonCustodialPSM public usdcNonCustodialPsm; - NonCustodialPSM public daiNonCustodialPsm; - ERC20Allocator public allocator; - - /// PCV Movement - SystemEntry public systemEntry; - MakerPCVSwapper public pcvSwapperMaker; - PCVGuardian public pcvGuardian; - PCVRouter public pcvRouter; - - /// Accounting - PCVOracle public pcvOracle; - ConstantPriceOracle public daiConstantOracle; - ConstantPriceOracle public usdcConstantOracle; - - /// Parameters - uint256 public constant TIMELOCK_DELAY = 86400; - - /// ---------- RATE LIMITED MINTER PARAMS ---------- - - /// maximum rate limit per second is 100 VOLT - uint256 public constant MAX_RATE_LIMIT_PER_SECOND_MINTING = 100e18; - - /// replenish 0 VOLT per day - uint128 public constant RATE_LIMIT_PER_SECOND_MINTING = 0; - - /// buffer cap of 3m VOLT - uint128 public constant BUFFER_CAP_MINTING = 3_000_000e18; - - /// ---------- RATE LIMITED MINTER PARAMS ---------- - - /// maximum rate limit per second is $100 - uint256 public constant MAX_RATE_LIMIT_PER_SECOND_EXITING = 100e18; - - /// replenish 500k VOLT per day ($5.787 dollars per second) - uint128 public constant RATE_LIMIT_PER_SECOND_EXIT = 5787037037037037000; - - /// buffer cap of 1.5m VOLT - uint128 public constant BUFFER_CAP_EXITING = 500_000e18; - - /// ---------- ALLOCATOR PARAMS ---------- - - uint256 public constant ALLOCATOR_MAX_RATE_LIMIT_PER_SECOND = 1_000e18; /// 1k volt per second - uint128 public constant ALLOCATOR_RATE_LIMIT_PER_SECOND = 10e18; /// 10 volt per second - uint128 public constant ALLOCATOR_BUFFER_CAP = 1_000_000e18; /// buffer cap is 1m volt - - /// ---------- PSM PARAMS ---------- - - uint128 public constant VOLT_FLOOR_PRICE_USDC = 1.05e6; - uint128 public constant VOLT_CEILING_PRICE_USDC = 1.10e6; - uint128 public constant VOLT_FLOOR_PRICE_DAI = 1.05e18; - uint128 public constant VOLT_CEILING_PRICE_DAI = 1.10e18; - uint248 public constant USDC_TARGET_BALANCE = 10_000e6; - uint248 public constant DAI_TARGET_BALANCE = 10_000e18; - int8 public constant USDC_DECIMALS_NORMALIZER = 12; - int8 public constant DAI_DECIMALS_NORMALIZER = 0; - - /// ---------- ORACLE PARAMS ---------- - - uint40 public constant VOLT_APR_START_TIME = 1704096000; /// 2024-01-01 TODO change this based on when the system starts - uint200 public constant VOLT_START_PRICE = 1.05e18; /// TODO change this based on when the system starts - uint16 public constant VOLT_MONTHLY_BASIS_POINTS = 14; - - function deploy() public { - /// Core - core = new CoreV2(address(voltV1)); - - volt = new VoltV2(address(core)); - - lock = new GlobalReentrancyLock(address(core)); - - /// all addresses will be able to execute - address[] memory executorAddresses = new address[](0); - - address[] memory proposerCancellerAddresses = new address[](1); - proposerCancellerAddresses[0] = MainnetAddresses.GOVERNOR; - timelockController = new TimelockController( - TIMELOCK_DELAY, - proposerCancellerAddresses, - executorAddresses - ); - - grlm = new GlobalRateLimitedMinter( - address(core), - MAX_RATE_LIMIT_PER_SECOND_MINTING, - RATE_LIMIT_PER_SECOND_MINTING, - BUFFER_CAP_MINTING - ); - gserl = new GlobalSystemExitRateLimiter( - address(core), - MAX_RATE_LIMIT_PER_SECOND_EXITING, - RATE_LIMIT_PER_SECOND_EXIT, - BUFFER_CAP_EXITING - ); - - /// VOLT rate - vso = new VoltSystemOracle( - address(core), - VOLT_MONTHLY_BASIS_POINTS, - VOLT_APR_START_TIME, /// todo fill in actual value - VOLT_START_PRICE /// todo fetch this from the old oracle after warping forward 24 hours - ); - - /// PCV Deposits - morphoDaiPCVDeposit = new MorphoPCVDeposit( - address(core), - MainnetAddresses.CDAI, - address(dai), - MainnetAddresses.COMP, - MainnetAddresses.MORPHO, - MainnetAddresses.MORPHO_LENS - ); - - morphoUsdcPCVDeposit = new MorphoPCVDeposit( - address(core), - MainnetAddresses.CUSDC, - address(usdc), - MainnetAddresses.COMP, - MainnetAddresses.MORPHO, - MainnetAddresses.MORPHO_LENS - ); - - /// Peg Stability - daipsm = new PegStabilityModule( - address(core), - address(vso), - address(0), - 0, - false, - dai, - VOLT_FLOOR_PRICE_DAI, - VOLT_CEILING_PRICE_DAI - ); - usdcpsm = new PegStabilityModule( - address(core), - address(vso), - address(0), - -12, - false, - usdc, - VOLT_FLOOR_PRICE_USDC, - VOLT_CEILING_PRICE_USDC - ); - usdcNonCustodialPsm = new NonCustodialPSM( - address(core), - address(vso), - address(0), - -12, - false, - usdc, - VOLT_FLOOR_PRICE_USDC, - VOLT_CEILING_PRICE_USDC, - IPCVDeposit(address(morphoUsdcPCVDeposit)) - ); - daiNonCustodialPsm = new NonCustodialPSM( - address(core), - address(vso), - address(0), - 0, - false, - dai, - VOLT_FLOOR_PRICE_DAI, - VOLT_CEILING_PRICE_DAI, - IPCVDeposit(address(morphoDaiPCVDeposit)) - ); - allocator = new ERC20Allocator(address(core)); - - voltMigrator = new VoltMigrator(address(core), IVolt(address(volt))); - - migratorRouter = new MigratorRouter( - IVolt(address(volt)), - IVoltMigrator(address(voltMigrator)), - daipsm, - usdcpsm - ); - - /// PCV Movement - systemEntry = new SystemEntry(address(core)); - - pcvSwapperMaker = new MakerPCVSwapper(address(core)); - - address[] memory pcvGuardianSafeAddresses = new address[](4); - pcvGuardianSafeAddresses[0] = address(morphoDaiPCVDeposit); - pcvGuardianSafeAddresses[1] = address(morphoUsdcPCVDeposit); - pcvGuardianSafeAddresses[2] = address(usdcpsm); - pcvGuardianSafeAddresses[3] = address(daipsm); - - pcvGuardian = new PCVGuardian( - address(core), - address(timelockController), - pcvGuardianSafeAddresses - ); - - pcvRouter = new PCVRouter(address(core)); - - /// Accounting - pcvOracle = new PCVOracle(address(core)); - daiConstantOracle = new ConstantPriceOracle(address(core), 1e18); - usdcConstantOracle = new ConstantPriceOracle( - address(core), - 1e18 * 10 ** uint256(uint8(USDC_DECIMALS_NORMALIZER)) - ); - } - - function setUp(address deployer) public { - /// Set references in Core - core.setVolt(IVolt(address(volt))); - core.setGlobalRateLimitedMinter( - IGlobalRateLimitedMinter(address(grlm)) - ); - core.setGlobalSystemExitRateLimiter( - IGlobalSystemExitRateLimiter(address(gserl)) - ); - core.setPCVOracle(IPCVOracle(address(pcvOracle))); - core.setGlobalReentrancyLock(IGlobalReentrancyLock(address(lock))); - /// Grant Roles - core.grantGovernor(address(timelockController)); - core.grantGovernor(MainnetAddresses.GOVERNOR); /// team multisig - - core.grantPCVController(address(allocator)); - core.grantPCVController(address(pcvGuardian)); - core.grantPCVController(address(pcvRouter)); - core.grantPCVController(MainnetAddresses.GOVERNOR); /// team multisig - core.grantPCVController(address(daiNonCustodialPsm)); - core.grantPCVController(address(usdcNonCustodialPsm)); - - core.createRole(VoltRoles.PCV_MOVER, VoltRoles.GOVERNOR); - core.grantRole(VoltRoles.PCV_MOVER, MainnetAddresses.GOVERNOR); /// team multisig - - core.createRole(VoltRoles.LIQUID_PCV_DEPOSIT, VoltRoles.GOVERNOR); - core.createRole(VoltRoles.ILLIQUID_PCV_DEPOSIT, VoltRoles.GOVERNOR); - core.grantRole( - VoltRoles.LIQUID_PCV_DEPOSIT, - address(morphoDaiPCVDeposit) - ); - core.grantRole( - VoltRoles.LIQUID_PCV_DEPOSIT, - address(morphoUsdcPCVDeposit) - ); - - core.grantPCVGuard(MainnetAddresses.EOA_1); - core.grantPCVGuard(MainnetAddresses.EOA_2); - core.grantPCVGuard(MainnetAddresses.EOA_4); - - core.grantGuardian(address(pcvGuardian)); - - core.grantRateLimitedMinter(address(daipsm)); - core.grantRateLimitedMinter(address(usdcpsm)); - - core.grantRateLimitedRedeemer(address(daipsm)); - core.grantRateLimitedRedeemer(address(usdcpsm)); - core.grantRateLimitedRedeemer(address(daiNonCustodialPsm)); - core.grantRateLimitedRedeemer(address(usdcNonCustodialPsm)); - - core.grantSystemExitRateLimitDepleter(address(daiNonCustodialPsm)); - core.grantSystemExitRateLimitDepleter(address(usdcNonCustodialPsm)); - - core.grantSystemExitRateLimitReplenisher(address(allocator)); - - core.grantLocker(address(systemEntry)); - core.grantLocker(address(allocator)); - core.grantLocker(address(pcvOracle)); - core.grantLocker(address(daipsm)); - core.grantLocker(address(usdcpsm)); - core.grantLocker(address(morphoDaiPCVDeposit)); - core.grantLocker(address(morphoUsdcPCVDeposit)); - core.grantLocker(address(grlm)); - core.grantLocker(address(gserl)); - core.grantLocker(address(pcvRouter)); - core.grantLocker(address(pcvGuardian)); - core.grantLocker(address(daiNonCustodialPsm)); - core.grantLocker(address(usdcNonCustodialPsm)); - - core.grantMinter(address(grlm)); - - /// Allocator config - allocator.connectPSM( - address(usdcpsm), - USDC_TARGET_BALANCE, - USDC_DECIMALS_NORMALIZER - ); - allocator.connectPSM( - address(daipsm), - DAI_TARGET_BALANCE, - DAI_DECIMALS_NORMALIZER - ); - allocator.connectDeposit( - address(usdcpsm), - address(morphoUsdcPCVDeposit) - ); - allocator.connectDeposit(address(daipsm), address(morphoDaiPCVDeposit)); - - /// Configure PCV Oracle - address[] memory venues = new address[](2); - venues[0] = address(morphoDaiPCVDeposit); - venues[1] = address(morphoUsdcPCVDeposit); - - address[] memory oracles = new address[](2); - oracles[0] = address(daiConstantOracle); - oracles[1] = address(usdcConstantOracle); - - bool[] memory isLiquid = new bool[](2); - isLiquid[0] = true; - isLiquid[1] = true; - - pcvOracle.addVenues(venues, oracles, isLiquid); - - /// Configure PCV Router - address[] memory swappers = new address[](1); - swappers[0] = address(pcvSwapperMaker); - pcvRouter.addPCVSwappers(swappers); - - /// Allow all addresses to execute proposals once completed - timelockController.grantRole( - timelockController.EXECUTOR_ROLE(), - address(0) - ); - /// Cleanup - timelockController.renounceRole( - timelockController.TIMELOCK_ADMIN_ROLE(), - deployer - ); - core.revokeGovernor(deployer); - } -} diff --git a/contracts/test/integration/IntegrationTestSystemV2.t.sol b/contracts/test/integration/IntegrationTestSystemV2.t.sol deleted file mode 100644 index ee1bd6137..000000000 --- a/contracts/test/integration/IntegrationTestSystemV2.t.sol +++ /dev/null @@ -1,1390 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.13; - -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; - -import {Vm} from "../unit/utils/Vm.sol"; -import {Test} from "../../../forge-std/src/Test.sol"; - -// import everything from SystemV2 -import "../../deployment/SystemV2.sol"; -import {stdError} from "../unit/utils/StdLib.sol"; -import {VoltRoles} from "../../core/VoltRoles.sol"; -import {VoltMigrator} from "../../volt/VoltMigrator.sol"; -import {MigratorRouter} from "../../pcv/MigratorRouter.sol"; -import {TestAddresses as addresses} from "../unit/utils/TestAddresses.sol"; - -contract IntegrationTestSystemV2 is Test { - using SafeCast for *; - SystemV2 systemV2; - address public constant user = 0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7; - uint224 public constant mintAmount = 100_000_000e18; - - CoreV2 core; - IERC20 dai; - IERC20 usdc; - IVolt oldVolt = IVolt(MainnetAddresses.VOLT); - VoltV2 volt; - NonCustodialPSM daincpsm; - PegStabilityModule daipsm; - NonCustodialPSM usdcncpsm; - PegStabilityModule usdcpsm; - GlobalRateLimitedMinter grlm; - GlobalSystemExitRateLimiter gserl; - ERC20Allocator allocator; - PCVOracle pcvOracle; - TimelockController timelockController; - PCVGuardian pcvGuardian; - MorphoPCVDeposit morphoDaiPCVDeposit; - MorphoPCVDeposit morphoUsdcPCVDeposit; - PCVRouter pcvRouter; - SystemEntry systemEntry; - VoltMigrator voltMigrator; - MigratorRouter migratorRouter; - - function setUp() public { - systemV2 = new SystemV2(); - systemV2.deploy(); - systemV2.setUp(address(systemV2)); - core = systemV2.core(); - dai = systemV2.dai(); - usdc = systemV2.usdc(); - volt = systemV2.volt(); - grlm = systemV2.grlm(); - gserl = systemV2.gserl(); - daipsm = systemV2.daipsm(); - daincpsm = systemV2.daiNonCustodialPsm(); - usdcncpsm = systemV2.usdcNonCustodialPsm(); - usdcpsm = systemV2.usdcpsm(); - allocator = systemV2.allocator(); - pcvOracle = systemV2.pcvOracle(); - timelockController = systemV2.timelockController(); - pcvGuardian = systemV2.pcvGuardian(); - morphoDaiPCVDeposit = systemV2.morphoDaiPCVDeposit(); - morphoUsdcPCVDeposit = systemV2.morphoUsdcPCVDeposit(); - pcvRouter = systemV2.pcvRouter(); - systemEntry = systemV2.systemEntry(); - voltMigrator = systemV2.voltMigrator(); - migratorRouter = systemV2.migratorRouter(); - - uint256 BUFFER_CAP_MINTING = systemV2.BUFFER_CAP_MINTING(); - uint256 amount = BUFFER_CAP_MINTING / 2; - - deal(address(dai), user, amount); - vm.label(address(dai), "dai"); - vm.label(address(usdc), "usdc"); - vm.label(address(volt), "new volt"); - vm.label(address(daipsm), "daipsm"); - vm.label(address(usdcpsm), "usdcpsm"); - vm.label(address(oldVolt), "old volt"); - vm.label(address(this), "address this"); - vm.label(address(voltMigrator), "Volt Migrator"); - vm.label(address(migratorRouter), "Migrator Router"); - } - - /* - Validate that the smart contracts are correctly linked to each other. - */ - function testLinks() public { - // core references - assertEq(address(core.volt()), address(systemV2.volt())); - assertEq(address(core.vcon()), address(0)); - assertEq( - address(core.globalRateLimitedMinter()), - address(systemV2.grlm()) - ); - assertEq( - address(core.globalSystemExitRateLimiter()), - address(systemV2.gserl()) - ); - assertEq(address(core.pcvOracle()), address(systemV2.pcvOracle())); - - // psm allocator - assertEq( - allocator.pcvDepositToPSM(address(systemV2.morphoUsdcPCVDeposit())), - address(systemV2.usdcpsm()) - ); - assertEq( - allocator.pcvDepositToPSM(address(systemV2.morphoDaiPCVDeposit())), - address(systemV2.daipsm()) - ); - (address psmToken1, , ) = allocator.allPSMs(address(systemV2.daipsm())); - (address psmToken2, , ) = allocator.allPSMs( - address(systemV2.usdcpsm()) - ); - assertEq(psmToken1, address(systemV2.dai())); - assertEq(psmToken2, address(systemV2.usdc())); - - // pcv oracle - assertEq(pcvOracle.getAllVenues().length, 2); - assertEq( - pcvOracle.getAllVenues()[0], - address(systemV2.morphoDaiPCVDeposit()) - ); - assertEq( - pcvOracle.getAllVenues()[1], - address(systemV2.morphoUsdcPCVDeposit()) - ); - - // pcv router - assertTrue(pcvRouter.isPCVSwapper(address(systemV2.pcvSwapperMaker()))); - } - - /* - Test that the roles are properly configured in the new system and that no - additional roles are granted to unexpected addresses. - */ - function testRoles() public { - // GOVERNOR - assertEq(core.getRoleMemberCount(VoltRoles.GOVERNOR), 3); - assertEq(core.getRoleMember(VoltRoles.GOVERNOR, 0), address(core)); - assertEq( - core.getRoleMember(VoltRoles.GOVERNOR, 1), - MainnetAddresses.GOVERNOR - ); - assertEq( - core.getRoleMember(VoltRoles.GOVERNOR, 2), - address(systemV2.timelockController()) - ); - - // PCV_CONTROLLER - assertEq(core.getRoleMemberCount(VoltRoles.PCV_CONTROLLER), 6); - assertEq( - core.getRoleMember(VoltRoles.PCV_CONTROLLER, 0), - address(systemV2.allocator()) - ); - assertEq( - core.getRoleMember(VoltRoles.PCV_CONTROLLER, 1), - address(systemV2.pcvGuardian()) - ); - assertEq( - core.getRoleMember(VoltRoles.PCV_CONTROLLER, 2), - address(systemV2.pcvRouter()) - ); - assertEq( - core.getRoleMember(VoltRoles.PCV_CONTROLLER, 3), - MainnetAddresses.GOVERNOR - ); - assertEq( - core.getRoleMember(VoltRoles.PCV_CONTROLLER, 4), - address(systemV2.daiNonCustodialPsm()) - ); - assertEq( - core.getRoleMember(VoltRoles.PCV_CONTROLLER, 5), - address(systemV2.usdcNonCustodialPsm()) - ); - - // PCV_MOVER - assertEq(core.getRoleMemberCount(VoltRoles.PCV_MOVER), 1); - assertEq( - core.getRoleMember(VoltRoles.PCV_MOVER, 0), - MainnetAddresses.GOVERNOR - ); - - // LIQUID_PCV_DEPOSIT_ROLE - assertEq(core.getRoleMemberCount(VoltRoles.LIQUID_PCV_DEPOSIT), 2); - assertEq( - core.getRoleMember(VoltRoles.LIQUID_PCV_DEPOSIT, 0), - address(systemV2.morphoDaiPCVDeposit()) - ); - assertEq( - core.getRoleMember(VoltRoles.LIQUID_PCV_DEPOSIT, 1), - address(systemV2.morphoUsdcPCVDeposit()) - ); - - // ILLIQUID_PCV_DEPOSIT_ROLE - assertEq(core.getRoleMemberCount(VoltRoles.ILLIQUID_PCV_DEPOSIT), 0); - - // PCV_GUARD - assertEq(core.getRoleMemberCount(VoltRoles.PCV_GUARD), 3); - assertEq( - core.getRoleMember(VoltRoles.PCV_GUARD, 0), - MainnetAddresses.EOA_1 - ); - assertEq( - core.getRoleMember(VoltRoles.PCV_GUARD, 1), - MainnetAddresses.EOA_2 - ); - assertEq( - core.getRoleMember(VoltRoles.PCV_GUARD, 2), - MainnetAddresses.EOA_4 - ); - - // GUARDIAN - assertEq(core.getRoleMemberCount(VoltRoles.GUARDIAN), 1); - assertEq( - core.getRoleMember(VoltRoles.GUARDIAN, 0), - address(systemV2.pcvGuardian()) - ); - - // RATE_LIMIT_SYSTEM_ENTRY_DEPLETE_ROLE - assertEq( - core.getRoleMemberCount(VoltRoles.RATE_LIMIT_SYSTEM_ENTRY_DEPLETE), - 2 - ); - assertEq( - core.getRoleMember(VoltRoles.RATE_LIMIT_SYSTEM_ENTRY_DEPLETE, 0), - address(systemV2.daipsm()) - ); - assertEq( - core.getRoleMember(VoltRoles.RATE_LIMIT_SYSTEM_ENTRY_DEPLETE, 1), - address(systemV2.usdcpsm()) - ); - - // RATE_LIMIT_SYSTEM_ENTRY_REPLENISH_ROLE - assertEq( - core.getRoleMemberCount( - VoltRoles.RATE_LIMIT_SYSTEM_ENTRY_REPLENISH - ), - 4 - ); - assertEq( - core.getRoleMember(VoltRoles.RATE_LIMIT_SYSTEM_ENTRY_REPLENISH, 0), - address(systemV2.daipsm()) - ); - assertEq( - core.getRoleMember(VoltRoles.RATE_LIMIT_SYSTEM_ENTRY_REPLENISH, 1), - address(systemV2.usdcpsm()) - ); - assertEq( - core.getRoleMember(VoltRoles.RATE_LIMIT_SYSTEM_ENTRY_REPLENISH, 2), - address(systemV2.daiNonCustodialPsm()) - ); - assertEq( - core.getRoleMember(VoltRoles.RATE_LIMIT_SYSTEM_ENTRY_REPLENISH, 3), - address(systemV2.usdcNonCustodialPsm()) - ); - - // LOCKER_ROLE - assertEq(core.getRoleMemberCount(VoltRoles.LOCKER), 13); - assertEq( - core.getRoleMember(VoltRoles.LOCKER, 0), - address(systemV2.systemEntry()) - ); - assertEq( - core.getRoleMember(VoltRoles.LOCKER, 1), - address(systemV2.allocator()) - ); - assertEq( - core.getRoleMember(VoltRoles.LOCKER, 2), - address(systemV2.pcvOracle()) - ); - assertEq( - core.getRoleMember(VoltRoles.LOCKER, 3), - address(systemV2.daipsm()) - ); - assertEq( - core.getRoleMember(VoltRoles.LOCKER, 4), - address(systemV2.usdcpsm()) - ); - assertEq( - core.getRoleMember(VoltRoles.LOCKER, 5), - address(systemV2.morphoDaiPCVDeposit()) - ); - assertEq( - core.getRoleMember(VoltRoles.LOCKER, 6), - address(systemV2.morphoUsdcPCVDeposit()) - ); - assertEq( - core.getRoleMember(VoltRoles.LOCKER, 7), - address(systemV2.grlm()) - ); - assertEq( - core.getRoleMember(VoltRoles.LOCKER, 8), - address(systemV2.gserl()) - ); - assertEq( - core.getRoleMember(VoltRoles.LOCKER, 9), - address(systemV2.pcvRouter()) - ); - assertEq( - core.getRoleMember(VoltRoles.LOCKER, 10), - address(systemV2.pcvGuardian()) - ); - assertEq(core.getRoleMember(VoltRoles.LOCKER, 11), address(daincpsm)); - assertEq(core.getRoleMember(VoltRoles.LOCKER, 12), address(usdcncpsm)); - - // MINTER - assertEq(core.getRoleMemberCount(VoltRoles.MINTER), 1); - assertEq( - core.getRoleMember(VoltRoles.MINTER, 0), - address(systemV2.grlm()) - ); - - /// SYSTEM EXIT RATE LIMIT DEPLETER - assertEq( - core.getRoleMemberCount(VoltRoles.RATE_LIMIT_SYSTEM_EXIT_DEPLETE), - 2 - ); - assertEq( - core.getRoleMember(VoltRoles.RATE_LIMIT_SYSTEM_EXIT_DEPLETE, 0), - address(systemV2.daiNonCustodialPsm()) - ); - assertEq( - core.getRoleMember(VoltRoles.RATE_LIMIT_SYSTEM_EXIT_DEPLETE, 1), - address(systemV2.usdcNonCustodialPsm()) - ); - - /// SYSTEM EXIT RATE LIMIT REPLENISH - assertEq( - core.getRoleMemberCount(VoltRoles.RATE_LIMIT_SYSTEM_EXIT_DEPLETE), - 2 - ); - assertEq( - core.getRoleMember(VoltRoles.RATE_LIMIT_SYSTEM_EXIT_REPLENISH, 0), - address(systemV2.allocator()) - ); - } - - function testTimelockRoles() public { - bytes32 executor = timelockController.EXECUTOR_ROLE(); - bytes32 canceller = timelockController.CANCELLER_ROLE(); - bytes32 proposer = timelockController.PROPOSER_ROLE(); - - assertTrue(timelockController.hasRole(executor, address(0))); /// role open - assertTrue( - timelockController.hasRole(canceller, MainnetAddresses.GOVERNOR) - ); - assertTrue( - timelockController.hasRole(proposer, MainnetAddresses.GOVERNOR) - ); - assertTrue(!timelockController.hasRole(canceller, address(0))); /// role closed - assertTrue(!timelockController.hasRole(proposer, address(0))); /// role closed - } - - function testMultisigProposesTimelock() public { - uint256 ethSendAmount = 100 ether; - vm.deal(address(timelockController), ethSendAmount); - - assertEq(address(timelockController).balance, ethSendAmount); /// starts with 0 balance - - bytes memory data = ""; - bytes32 predecessor = bytes32(0); - bytes32 salt = bytes32(0); - address recipient = address(100); - - vm.prank(MainnetAddresses.GOVERNOR); - timelockController.schedule( - recipient, - ethSendAmount, - data, - predecessor, - salt, - 86400 - ); - bytes32 id = timelockController.hashOperation( - recipient, - ethSendAmount, - data, - predecessor, - salt - ); - - uint256 startingEthBalance = recipient.balance; - - assertTrue(!timelockController.isOperationDone(id)); /// operation is not done - assertTrue(!timelockController.isOperationReady(id)); /// operation is not ready - - vm.warp(block.timestamp + timelockController.getMinDelay()); - assertTrue(timelockController.isOperationReady(id)); /// operation is ready - - timelockController.execute( - recipient, - ethSendAmount, - data, - predecessor, - salt - ); - - assertTrue(timelockController.isOperationDone(id)); /// operation is done - - assertEq(address(timelockController).balance, 0); - assertEq(recipient.balance, ethSendAmount + startingEthBalance); /// assert receiver received their eth - } - - function testOraclePriceAndReference() public { - address oracleAddress = address(systemV2.vso()); - - assertEq(address(systemV2.daipsm().oracle()), oracleAddress); - assertEq(address(systemV2.usdcpsm().oracle()), oracleAddress); - - assertEq( - systemV2.vso().getCurrentOraclePrice(), - systemV2.VOLT_START_PRICE() - ); - } - - function testCoreReferences() public { - address coreAddress = address(core); - - assertEq(address(systemV2.daipsm().core()), coreAddress); - assertEq(address(systemV2.usdcpsm().core()), coreAddress); - assertEq(address(systemV2.volt().core()), coreAddress); - assertEq(address(systemV2.grlm().core()), coreAddress); - assertEq(address(systemV2.gserl().core()), coreAddress); - assertEq(address(systemV2.vso().core()), coreAddress); - assertEq(address(systemV2.morphoDaiPCVDeposit().core()), coreAddress); - assertEq(address(systemV2.morphoUsdcPCVDeposit().core()), coreAddress); - assertEq(address(systemV2.usdcNonCustodialPsm().core()), coreAddress); - assertEq(address(systemV2.daiNonCustodialPsm().core()), coreAddress); - assertEq(address(systemV2.allocator().core()), coreAddress); - assertEq(address(systemV2.systemEntry().core()), coreAddress); - assertEq(address(systemV2.pcvSwapperMaker().core()), coreAddress); - assertEq(address(systemV2.pcvGuardian().core()), coreAddress); - assertEq(address(systemV2.pcvRouter().core()), coreAddress); - assertEq(address(systemV2.pcvOracle().core()), coreAddress); - assertEq(address(systemV2.daiConstantOracle().core()), coreAddress); - assertEq(address(systemV2.usdcConstantOracle().core()), coreAddress); - } - - /* - Flow of the first user that mints VOLT in the new system. - Performs checks on the global rate limits, and accounting - in the new system's PCV Oracle. - */ - function testFirstUserMint() public { - // setup variables - uint256 BUFFER_CAP_MINTING = systemV2.BUFFER_CAP_MINTING(); - uint256 amount = BUFFER_CAP_MINTING / 2; - uint256 daiTargetBalance = systemV2.DAI_TARGET_BALANCE(); - uint256 expectDaiBalance = amount - daiTargetBalance; - - // at system deloy, buffer is full - assertEq(grlm.buffer(), BUFFER_CAP_MINTING); - - { - // at system deploy, pcv is 0 - ( - uint256 liquidPcv1, - uint256 illiquidPcv1, - uint256 totalPcv1 - ) = pcvOracle.getTotalPcv(); - assertEq(liquidPcv1, 0); - assertEq(illiquidPcv1, 0); - assertEq(totalPcv1, 0); - } - - // user performs the first mint - vm.startPrank(user); - dai.approve(address(daipsm), amount); - daipsm.mint(user, amount, 0); - vm.stopPrank(); - - // buffer has been used - uint256 voltReceived1 = volt.balanceOf(user); - assertEq(grlm.buffer(), BUFFER_CAP_MINTING - voltReceived1); - - // user received VOLT - assertTrue(voltReceived1 > (90 * amount) / 100); - // psm received DAI - assertEq(daipsm.balance(), amount); - - allocator.skim(address(morphoDaiPCVDeposit)); - { - // after first mint, pcv is = amount - ( - uint256 liquidPcv2, - uint256 illiquidPcv2, - uint256 totalPcv2 - ) = pcvOracle.getTotalPcv(); - assertApproxEq( - liquidPcv2.toInt256(), - expectDaiBalance.toInt256(), - 0 - ); - assertApproxEq( - totalPcv2.toInt256(), - expectDaiBalance.toInt256(), - 0 - ); - assertEq(illiquidPcv2, 0); - } - - // user performs the second mint - vm.startPrank(user); - usdc.approve(address(usdcpsm), amount / 1e12); - usdcpsm.mint(user, amount / 1e12, 0); - vm.stopPrank(); - uint256 voltReceived2 = volt.balanceOf(user) - voltReceived1; - - // buffer has been used - assertEq( - grlm.buffer(), - systemV2.BUFFER_CAP_MINTING() - voltReceived1 - voltReceived2 - ); - - // user received VOLT - assertTrue(voltReceived2 > (90 * amount) / 100); - // psm received USDC - assertEq(usdcpsm.balance(), amount / 1e12); - - allocator.skim(address(morphoUsdcPCVDeposit)); - { - // after second mint, pcv is = 2 * amount - ( - uint256 liquidPcv3, - uint256 illiquidPcv3, - uint256 totalPcv3 - ) = pcvOracle.getTotalPcv(); - assertEq(illiquidPcv3, 0); - assertApproxEq( - totalPcv3.toInt256(), - 2 * expectDaiBalance.toInt256(), - 0 - ); - assertApproxEq( - liquidPcv3.toInt256(), - 2 * expectDaiBalance.toInt256(), - 0 - ); - } - vm.snapshot(); - - vm.prank(address(core)); - grlm.setRateLimitPerSecond(5.787e18); - - // buffer replenishes over time - vm.warp(block.timestamp + 3 days); - - // above limit rate reverts - vm.startPrank(user); - dai.approve(address(daipsm), BUFFER_CAP_MINTING * 2); - vm.expectRevert("RateLimited: rate limit hit"); - daipsm.mint(user, BUFFER_CAP_MINTING * 2, 0); - vm.stopPrank(); - } - - function testRedeemsDaiPsm(uint88 voltAmount) public { - testFirstUserMint(); - - uint256 snapshotId = 0; /// roll back to before buffer replenished - vm.revertTo(snapshotId); - vm.assume(voltAmount <= volt.balanceOf(user)); - - { - uint256 daiAmountOut = daipsm.getRedeemAmountOut(voltAmount); - deal(address(dai), address(daipsm), daiAmountOut); - - vm.startPrank(user); - - uint256 startingBuffer = grlm.buffer(); - uint256 startingDaiBalance = dai.balanceOf(user); - - volt.approve(address(daipsm), voltAmount); - daipsm.redeem(user, voltAmount, daiAmountOut); - - uint256 endingBuffer = grlm.buffer(); - uint256 endingDaiBalance = dai.balanceOf(user); - - assertEq(endingDaiBalance - startingDaiBalance, daiAmountOut); - assertEq(endingBuffer - startingBuffer, voltAmount); - - vm.stopPrank(); - } - } - - function testRedeemsDaiNcPsm(uint80 voltRedeemAmount) public { - vm.assume(voltRedeemAmount >= 1e18); - vm.assume(voltRedeemAmount <= 475_000e18); - testFirstUserMint(); - - uint256 snapshotId = 0; /// roll back to before buffer replenished - vm.revertTo(snapshotId); - - { - uint256 daiAmountOut = daincpsm.getRedeemAmountOut( - voltRedeemAmount - ); - deal(address(dai), address(morphoDaiPCVDeposit), daiAmountOut * 2); - systemEntry.deposit(address(morphoDaiPCVDeposit)); - - vm.startPrank(user); - - uint256 startingBuffer = grlm.buffer(); - uint256 startingExitBuffer = gserl.buffer(); - uint256 startingDaiBalance = dai.balanceOf(user); - - volt.approve(address(daincpsm), voltRedeemAmount); - daincpsm.redeem(user, voltRedeemAmount, daiAmountOut); - - uint256 endingBuffer = grlm.buffer(); - uint256 endingExitBuffer = gserl.buffer(); - uint256 endingDaiBalance = dai.balanceOf(user); - - assertEq(endingBuffer - startingBuffer, voltRedeemAmount); /// grlm buffer replenished - assertEq(endingDaiBalance - startingDaiBalance, daiAmountOut); - assertEq(startingExitBuffer - endingExitBuffer, daiAmountOut); /// exit buffer depleted - - vm.stopPrank(); - } - } - - function testRedeemsUsdcNcPsm(uint80 voltRedeemAmount) public { - vm.assume(voltRedeemAmount >= 1e18); - vm.assume(voltRedeemAmount <= 475_000e18); - testFirstUserMint(); - - uint256 snapshotId = 0; /// roll back to before buffer replenished - vm.revertTo(snapshotId); - - { - uint256 usdcAmountOut = usdcncpsm.getRedeemAmountOut( - voltRedeemAmount - ); - deal( - address(usdc), - address(morphoUsdcPCVDeposit), - usdcAmountOut * 2 - ); - systemEntry.deposit(address(morphoUsdcPCVDeposit)); - - vm.startPrank(user); - - uint256 startingBuffer = grlm.buffer(); - uint256 startingExitBuffer = gserl.buffer(); - uint256 startingUsdcBalance = usdc.balanceOf(user); - - volt.approve(address(usdcncpsm), voltRedeemAmount); - usdcncpsm.redeem(user, voltRedeemAmount, usdcAmountOut); - - uint256 endingBuffer = grlm.buffer(); - uint256 endingExitBuffer = gserl.buffer(); - uint256 endingUsdcBalance = usdc.balanceOf(user); - - assertEq(endingBuffer - startingBuffer, voltRedeemAmount); /// buffer replenished - assertEq(endingUsdcBalance - startingUsdcBalance, usdcAmountOut); - assertEq( - startingExitBuffer - endingExitBuffer, - usdcAmountOut * 1e12 - ); /// ensure buffer adjusted up 12 decimals, buffer depleted - - vm.stopPrank(); - } - } - - function testRedeemsUsdcPsm(uint80 voltRedeemAmount) public { - vm.assume(voltRedeemAmount >= 1e18); - vm.assume(voltRedeemAmount <= 475_000e18); - testFirstUserMint(); - - uint256 snapshotId = 0; /// roll back to before buffer replenished - vm.revertTo(snapshotId); - - { - uint256 usdcAmountOut = usdcpsm.getRedeemAmountOut( - voltRedeemAmount - ); - deal(address(usdc), address(usdcpsm), usdcAmountOut); - - vm.startPrank(user); - - uint256 startingBuffer = grlm.buffer(); - uint256 startingUsdcBalance = usdc.balanceOf(user); - - volt.approve(address(usdcpsm), voltRedeemAmount); - usdcpsm.redeem(user, voltRedeemAmount, usdcAmountOut); - - uint256 endingBuffer = grlm.buffer(); - uint256 endingUsdcBalance = usdc.balanceOf(user); - - assertEq(endingBuffer - startingBuffer, voltRedeemAmount); - assertEq(endingUsdcBalance - startingUsdcBalance, usdcAmountOut); - - vm.stopPrank(); - } - } - - /* - Migrate PCV from current system to V2 system, and perform sanity checks on amounts. - */ - function testMigratePcv() public { - uint256 migratedPcv = _migratePcv(); - - // get PCV stats - (uint256 liquidPcv, uint256 illiquidPcv, uint256 totalPcv) = systemV2 - .pcvOracle() - .getTotalPcv(); - - // sanity check - assertEq(liquidPcv, totalPcv); - assertEq(illiquidPcv, 0); - assertGt(totalPcv, 1_500_000e18); - assertGt(migratedPcv, 1_500_000e18); - } - - /* - After migrating to V2 system, check that we can use PCVGuardian. - */ - function testPcvGuardian() public { - _migratePcv(); - uint256 amount = 100_000e18; - - uint256 depositDaiBalanceBefore = morphoDaiPCVDeposit.balance(); - uint256 safeAddressDaiBalanceBefore = dai.balanceOf( - address(timelockController) - ); - - vm.prank(MainnetAddresses.GOVERNOR); - pcvGuardian.withdrawToSafeAddress(address(morphoDaiPCVDeposit), amount); - - uint256 depositDaiBalanceAfter = morphoDaiPCVDeposit.balance(); - uint256 safeAddressDaiBalanceAfter = dai.balanceOf( - address(timelockController) - ); - - // tolerate 0.5% err because morpho withdrawals are not exact - assertGt( - safeAddressDaiBalanceAfter - safeAddressDaiBalanceBefore, - (995 * amount) / 1000 - ); - assertGt( - depositDaiBalanceBefore - depositDaiBalanceAfter, - (995 * amount) / 1000 - ); - } - - /* - After migrating to V2 system, check that we can use PCVRouter + MakerPCVSwapper. - Swap DAI to USDC, then USDC to DAI. - */ - function testPcvRouterWithSwap() public { - _migratePcv(); - uint256 amount = 100_000e18; - - uint256 depositDaiBalanceBefore = morphoDaiPCVDeposit.balance(); - uint256 depositUsdcBalanceBefore = morphoUsdcPCVDeposit.balance(); - - // Swap DAI to USDC - vm.startPrank(MainnetAddresses.GOVERNOR); // has PCV_MOVER role - pcvRouter.movePCV( - address(morphoDaiPCVDeposit), // source - address(morphoUsdcPCVDeposit), // destination - address(systemV2.pcvSwapperMaker()), // swapper - amount, // amount - address(systemV2.dai()), // sourceAsset - address(systemV2.usdc()), // destinationAsset - true, // sourceIsLiquid - true // destinationIsLiquid - ); - vm.stopPrank(); - - uint256 depositDaiBalanceAfter = morphoDaiPCVDeposit.balance(); - uint256 depositUsdcBalanceAfter = morphoUsdcPCVDeposit.balance(); - - // tolerate 0.5% err because morpho withdrawals are not exact - assertGt( - depositDaiBalanceBefore - depositDaiBalanceAfter, - (995 * amount) / 1000 - ); - assertGt( - depositUsdcBalanceAfter - depositUsdcBalanceBefore, - ((995 * amount) / 1e12) / 1000 - ); - - // Swap USDC to DAI (half of previous amount) - vm.startPrank(MainnetAddresses.GOVERNOR); // has PCV_MOVER role - pcvRouter.movePCV( - address(morphoUsdcPCVDeposit), // source - address(morphoDaiPCVDeposit), // destination - address(systemV2.pcvSwapperMaker()), // swapper - amount / 2e12, // amount - address(systemV2.usdc()), // sourceAsset - address(systemV2.dai()), // destinationAsset - true, // sourceIsLiquid - true // destinationIsLiquid - ); - vm.stopPrank(); - - uint256 depositDaiBalanceFinal = morphoDaiPCVDeposit.balance(); - uint256 depositUsdcBalanceFinal = morphoUsdcPCVDeposit.balance(); - - // tolerate 0.5% err because morpho withdrawals are not exact - assertGt( - depositDaiBalanceFinal - depositDaiBalanceAfter, - (995 * amount) / 2000 - ); - assertGt( - depositUsdcBalanceAfter - depositUsdcBalanceFinal, - ((995 * amount) / 1e12) / 2000 - ); - } - - /* - After migrating to V2 system, check that we can unset the PCVOracle in Core - and that it doesn't break PCV movements (only disables accounting). - */ - function testUnsetPcvOracle() public { - _migratePcv(); - - vm.prank(MainnetAddresses.GOVERNOR); - core.setPCVOracle(IPCVOracle(address(0))); - - vm.prank(MainnetAddresses.GOVERNOR); - pcvGuardian.withdrawToSafeAddress(address(morphoDaiPCVDeposit), 100e18); - - // No revert & PCV moved - assertEq(dai.balanceOf(pcvGuardian.safeAddress()), 100e18); - - // User redeems - vm.prank(address(systemV2.grlm())); - volt.mint(address(this), 100e18); - volt.approve(address(daipsm), 100e18); - daipsm.redeem(address(this), 100e18, 104e18); - assertGt(dai.balanceOf(address(this)), 104e18); - } - - /* - Internal helper function, migrate the PCV from current system to V2 system - */ - function _migratePcv() internal returns (uint256) { - MorphoPCVDeposit oldMorphoDaiPCVDeposit = MorphoPCVDeposit( - MainnetAddresses.MORPHO_COMPOUND_DAI_PCV_DEPOSIT - ); - /*MorphoPCVDeposit oldMorphoUsdcPCVDeposit = MorphoPCVDeposit( - MainnetAddresses.MORPHO_COMPOUND_USDC_PCV_DEPOSIT - );*/ - PCVGuardian oldPCVGuardian = PCVGuardian(MainnetAddresses.PCV_GUARDIAN); - - uint256 daiBalanceBefore = dai.balanceOf(MainnetAddresses.GOVERNOR); - uint256 usdcBalanceBefore = usdc.balanceOf(MainnetAddresses.GOVERNOR); - - // Move all DAI and USDC to Safe Address - vm.startPrank(MainnetAddresses.GOVERNOR); - oldPCVGuardian.withdrawAllToSafeAddress( - address(oldMorphoDaiPCVDeposit) - ); - //oldPCVGuardian.withdrawAllToSafeAddress(address(oldMorphoUsdcPCVDeposit)); // reverts because 0 - oldPCVGuardian.withdrawAllERC20ToSafeAddress( - MainnetAddresses.VOLT_DAI_PSM, - MainnetAddresses.DAI - ); - oldPCVGuardian.withdrawAllERC20ToSafeAddress( - MainnetAddresses.VOLT_DAI_PSM, - MainnetAddresses.VOLT - ); - oldPCVGuardian.withdrawAllERC20ToSafeAddress( - MainnetAddresses.VOLT_USDC_PSM, - MainnetAddresses.USDC - ); - oldPCVGuardian.withdrawAllERC20ToSafeAddress( - MainnetAddresses.VOLT_USDC_PSM, - MainnetAddresses.VOLT - ); - vm.stopPrank(); - - uint256 daiBalanceAfter = dai.balanceOf(MainnetAddresses.GOVERNOR); - uint256 usdcBalanceAfter = usdc.balanceOf(MainnetAddresses.GOVERNOR); - uint256 protocolDai = daiBalanceAfter - daiBalanceBefore; - uint256 protocolUsdc = usdcBalanceAfter - usdcBalanceBefore; - - // Move DAI to expected location - vm.startPrank(MainnetAddresses.GOVERNOR); - dai.transfer(address(systemV2.daipsm()), 100_000e18); - dai.transfer( - address(systemV2.morphoDaiPCVDeposit()), - protocolDai - 100_000e18 - ); - usdc.transfer(address(systemV2.usdcpsm()), protocolUsdc); - systemEntry.deposit(address(systemV2.morphoDaiPCVDeposit())); - vm.stopPrank(); - - return protocolDai + protocolUsdc * 1e12; - } - - function _migratorSetup() private { - vm.prank(address(timelockController)); - core.grantMinter(addresses.minterAddress); - } - - function testMigratorSetup() public { - assertEq(address(migratorRouter.newVolt()), address(volt)); - assertEq(address(migratorRouter.OLD_VOLT()), address(oldVolt)); - } - - function testExchangeTo(uint64 amountOldVoltToExchange) public { - _migratorSetup(); - oldVolt.approve(address(voltMigrator), type(uint256).max); - deal(address(oldVolt), address(this), amountOldVoltToExchange); - deal(address(volt), address(voltMigrator), amountOldVoltToExchange); - - uint256 newVoltTotalSupply = volt.totalSupply(); - uint256 oldVoltTotalSupply = oldVolt.totalSupply(); - - uint256 oldVoltBalanceBefore = oldVolt.balanceOf(address(this)); - uint256 newVoltBalanceBefore = volt.balanceOf(address(0xFFF)); - - voltMigrator.exchangeTo(address(0xFFF), amountOldVoltToExchange); - - uint256 oldVoltBalanceAfter = oldVolt.balanceOf(address(this)); - uint256 newVoltBalanceAfter = volt.balanceOf(address(0xFFF)); - - assertEq( - newVoltBalanceAfter, - newVoltBalanceBefore + amountOldVoltToExchange - ); - assertEq( - oldVoltBalanceAfter, - oldVoltBalanceBefore - amountOldVoltToExchange - ); - assertEq( - oldVolt.totalSupply(), - oldVoltTotalSupply - amountOldVoltToExchange - ); - assertEq(volt.totalSupply(), newVoltTotalSupply); /// new volt supply remains unchanged - } - - function testExchangeAllTo() public { - _migratorSetup(); - uint256 amountOldVoltToExchange = 10_000e18; - - oldVolt.approve(address(voltMigrator), type(uint256).max); - - deal(address(oldVolt), address(this), amountOldVoltToExchange); - deal(address(volt), address(voltMigrator), amountOldVoltToExchange); - - uint256 newVoltTotalSupply = volt.totalSupply(); - uint256 oldVoltTotalSupply = oldVolt.totalSupply(); - - uint256 oldVoltBalanceBefore = oldVolt.balanceOf(address(this)); - uint256 newVoltBalanceBefore = volt.balanceOf(address(0xFFF)); - - voltMigrator.exchangeAllTo(address(0xFFF)); - - uint256 oldVoltBalanceAfter = oldVolt.balanceOf(address(this)); - uint256 newVoltBalanceAfter = volt.balanceOf(address(0xFFF)); - - assertEq( - newVoltBalanceAfter, - newVoltBalanceBefore + oldVoltBalanceBefore - ); - assertEq(oldVoltBalanceAfter, 0); - assertEq( - oldVolt.totalSupply(), - oldVoltTotalSupply - oldVoltBalanceBefore - ); - assertEq(volt.totalSupply(), newVoltTotalSupply); - } - - function testExchangeFailsWhenApprovalNotGiven() public { - vm.expectRevert("ERC20: burn amount exceeds allowance"); - voltMigrator.exchange(1e18); - } - - function testExchangeToFailsWhenApprovalNotGiven() public { - vm.expectRevert("ERC20: burn amount exceeds allowance"); - voltMigrator.exchangeTo(address(0xFFF), 1e18); - } - - function testExchangeFailsMigratorUnderfunded() public { - _migratorSetup(); - uint256 amountOldVoltToExchange = 100_000_000e18; - - vm.prank(addresses.minterAddress); - volt.mint(address(voltMigrator), amountOldVoltToExchange); - deal(address(oldVolt), address(this), amountOldVoltToExchange); - - oldVolt.approve(address(voltMigrator), type(uint256).max); - - vm.prank(address(voltMigrator)); - volt.burn(mintAmount); - - vm.expectRevert(stdError.arithmeticError); - voltMigrator.exchange(amountOldVoltToExchange); - } - - function testExchangeAllFailsMigratorUnderfunded() public { - _migratorSetup(); - - oldVolt.approve(address(voltMigrator), type(uint256).max); - deal(address(oldVolt), address(this), mintAmount); - - vm.prank(addresses.minterAddress); - volt.mint(address(voltMigrator), mintAmount); - - vm.prank(address(voltMigrator)); - volt.burn(mintAmount); - - vm.expectRevert(stdError.arithmeticError); - voltMigrator.exchangeAll(); - } - - function testExchangeToFailsMigratorUnderfunded() public { - _migratorSetup(); - - deal(address(oldVolt), address(this), mintAmount); - - vm.prank(addresses.minterAddress); - volt.mint(address(voltMigrator), mintAmount); - - uint256 amountOldVoltToExchange = 100_000_000e18; - oldVolt.approve(address(voltMigrator), type(uint256).max); - - vm.prank(address(voltMigrator)); - volt.burn(mintAmount); - - vm.expectRevert(stdError.arithmeticError); - voltMigrator.exchangeTo(address(0xFFF), amountOldVoltToExchange); - } - - function testExchangeAllToFailsMigratorUnderfunded() public { - _migratorSetup(); - - vm.prank(addresses.minterAddress); - volt.mint(address(voltMigrator), mintAmount); - deal(address(oldVolt), address(this), mintAmount); - - oldVolt.approve(address(voltMigrator), type(uint256).max); - - vm.prank(address(voltMigrator)); - volt.burn(mintAmount); - - vm.expectRevert(stdError.arithmeticError); - voltMigrator.exchangeAllTo(address(0xFFF)); - } - - function testExchangeAllWhenApprovalNotGiven() public { - _migratorSetup(); - - uint256 oldVoltBalanceBefore = oldVolt.balanceOf(address(this)); - uint256 newVoltBalanceBefore = volt.balanceOf(address(this)); - - vm.expectRevert("VoltMigrator: no amount to exchange"); - voltMigrator.exchangeAll(); - - uint256 oldVoltBalanceAfter = oldVolt.balanceOf(address(this)); - uint256 newVoltBalanceAfter = volt.balanceOf(address(this)); - - assertEq(oldVoltBalanceBefore, oldVoltBalanceAfter); - assertEq(newVoltBalanceBefore, newVoltBalanceAfter); - } - - function testExchangeAllToWhenApprovalNotGiven() public { - uint256 oldVoltBalanceBefore = oldVolt.balanceOf(address(this)); - uint256 newVoltBalanceBefore = volt.balanceOf(address(0xFFF)); - - vm.expectRevert("VoltMigrator: no amount to exchange"); - voltMigrator.exchangeAllTo(address(0xFFF)); - - uint256 oldVoltBalanceAfter = oldVolt.balanceOf(address(this)); - uint256 newVoltBalanceAfter = volt.balanceOf(address(0xFFF)); - - assertEq(oldVoltBalanceBefore, oldVoltBalanceAfter); - assertEq(newVoltBalanceBefore, newVoltBalanceAfter); - } - - function testExchangeAllPartialApproval() public { - _migratorSetup(); - - deal(address(oldVolt), address(this), 100_000e18); - - uint256 amountOldVoltToExchange = oldVolt.balanceOf(address(this)) / 2; // exchange half of users balance - - oldVolt.approve(address(voltMigrator), amountOldVoltToExchange); - - vm.prank(addresses.minterAddress); - volt.mint(address(voltMigrator), amountOldVoltToExchange); - - uint256 newVoltTotalSupply = volt.totalSupply(); - uint256 oldVoltTotalSupply = oldVolt.totalSupply(); - - uint256 oldVoltBalanceBefore = oldVolt.balanceOf(address(this)); - uint256 newVoltBalanceBefore = volt.balanceOf(address(this)); - - voltMigrator.exchangeAllTo(address(this)); - - uint256 oldVoltBalanceAfter = oldVolt.balanceOf(address(this)); - uint256 newVoltBalanceAfter = volt.balanceOf(address(this)); - - assertEq( - newVoltBalanceAfter, - newVoltBalanceBefore + amountOldVoltToExchange - ); - assertEq( - oldVoltBalanceAfter, - oldVoltBalanceBefore - amountOldVoltToExchange - ); - - assertEq( - oldVolt.totalSupply(), - oldVoltTotalSupply - amountOldVoltToExchange - ); - assertEq(volt.totalSupply(), newVoltTotalSupply); - - assertEq(oldVoltBalanceAfter, oldVoltBalanceBefore / 2); - } - - function testExchangeAllToPartialApproval() public { - _migratorSetup(); - - uint256 amountOldVoltToExchange = mintAmount / 2; // exchange half of users balance - oldVolt.approve(address(voltMigrator), amountOldVoltToExchange); - - vm.prank(addresses.minterAddress); - volt.mint(address(voltMigrator), mintAmount); - - vm.prank(MainnetAddresses.GOVERNOR); - CoreV2(MainnetAddresses.CORE).grantMinter(address(this)); - oldVolt.mint(address(this), mintAmount); - - uint256 newVoltTotalSupply = volt.totalSupply(); - uint256 oldVoltTotalSupply = oldVolt.totalSupply(); - - uint256 oldVoltBalanceBefore = oldVolt.balanceOf(address(this)); - uint256 newVoltBalanceBefore = volt.balanceOf(address(0xFFF)); - - voltMigrator.exchangeAllTo(address(0xFFF)); - - uint256 oldVoltBalanceAfter = oldVolt.balanceOf(address(this)); - uint256 newVoltBalanceAfter = volt.balanceOf(address(0xFFF)); - - assertEq( - newVoltBalanceAfter, - newVoltBalanceBefore + amountOldVoltToExchange - ); - assertEq( - oldVoltBalanceAfter, - oldVoltBalanceBefore - amountOldVoltToExchange - ); - assertEq( - oldVolt.totalSupply(), - oldVoltTotalSupply - amountOldVoltToExchange - ); - assertEq(volt.totalSupply(), newVoltTotalSupply); - assertEq(oldVoltBalanceAfter, oldVoltBalanceBefore / 2); - } - - function testSweep() public { - uint256 amountToTransfer = 1_000_000e6; - - uint256 startingBalance = usdc.balanceOf( - MainnetAddresses.TIMELOCK_CONTROLLER - ); - - deal(MainnetAddresses.USDC, address(voltMigrator), amountToTransfer); - - vm.prank(MainnetAddresses.GOVERNOR); - voltMigrator.sweep( - address(usdc), - MainnetAddresses.TIMELOCK_CONTROLLER, - amountToTransfer - ); - - uint256 endingBalance = usdc.balanceOf( - MainnetAddresses.TIMELOCK_CONTROLLER - ); - - assertEq(endingBalance - startingBalance, amountToTransfer); - } - - function testSweepNonGovernorFails() public { - uint256 amountToTransfer = 1_000_000e6; - - deal(MainnetAddresses.USDC, address(voltMigrator), amountToTransfer); - - vm.expectRevert("CoreRef: Caller is not a governor"); - voltMigrator.sweep( - address(usdc), - MainnetAddresses.TIMELOCK_CONTROLLER, - amountToTransfer - ); - } - - function testSweepNewVoltFails() public { - uint256 amountToSweep = volt.balanceOf(address(voltMigrator)); - - vm.startPrank(MainnetAddresses.GOVERNOR); - vm.expectRevert("VoltMigrator: cannot sweep new Volt"); - voltMigrator.sweep( - address(volt), - MainnetAddresses.TIMELOCK_CONTROLLER, - amountToSweep - ); - vm.stopPrank(); - } - - function testRedeemUsdc(uint72 amountVoltIn) public { - _migratorSetup(); - - oldVolt.approve(address(migratorRouter), type(uint256).max); - - vm.prank(addresses.minterAddress); - volt.mint(address(voltMigrator), amountVoltIn); - - vm.prank(MainnetAddresses.GOVERNOR); - CoreV2(MainnetAddresses.CORE).grantMinter(address(this)); - oldVolt.mint(address(this), amountVoltIn); - - uint256 startBalance = usdc.balanceOf(address(this)); - uint256 minAmountOut = usdcpsm.getRedeemAmountOut(amountVoltIn); - - deal(address(usdc), address(usdcpsm), minAmountOut); - - uint256 currentPegPrice = systemV2.vso().getCurrentOraclePrice() / 1e12; - uint256 amountOut = (amountVoltIn * currentPegPrice) / 1e18; - - uint256 redeemedAmount = migratorRouter.redeemUSDC( - amountVoltIn, - minAmountOut - ); - uint256 endBalance = usdc.balanceOf(address(this)); - - assertApproxEq(minAmountOut.toInt256(), amountOut.toInt256(), 0); - assertEq(minAmountOut, endBalance - startBalance); - assertEq(redeemedAmount, minAmountOut); - } - - function testRedeemDai(uint72 amountVoltIn) public { - _migratorSetup(); - - oldVolt.approve(address(migratorRouter), type(uint256).max); - - vm.prank(addresses.minterAddress); - volt.mint(address(voltMigrator), amountVoltIn); - - vm.prank(MainnetAddresses.GOVERNOR); - CoreV2(MainnetAddresses.CORE).grantMinter(address(this)); - oldVolt.mint(address(this), amountVoltIn); - - uint256 startBalance = dai.balanceOf(address(this)); - uint256 minAmountOut = daipsm.getRedeemAmountOut(amountVoltIn); - - deal(address(dai), address(daipsm), minAmountOut); - - uint256 currentPegPrice = systemV2.vso().getCurrentOraclePrice(); - uint256 amountOut = (amountVoltIn * currentPegPrice) / 1e18; - - uint256 redeemedAmount = migratorRouter.redeemDai( - amountVoltIn, - minAmountOut - ); - uint256 endBalance = dai.balanceOf(address(this)); - - assertApproxEq(minAmountOut.toInt256(), amountOut.toInt256(), 0); - assertEq(minAmountOut, endBalance - startBalance); - assertEq(redeemedAmount, minAmountOut); - } - - function testRedeemDaiFailsUserNotEnoughVolt() public { - oldVolt.approve(address(migratorRouter), type(uint256).max); - - uint256 minAmountOut = daipsm.getRedeemAmountOut(mintAmount); - - vm.expectRevert("ERC20: transfer amount exceeds balance"); - migratorRouter.redeemDai(mintAmount, minAmountOut); - } - - function testRedeemUsdcFailsUserNotEnoughVolt() public { - oldVolt.approve(address(migratorRouter), type(uint256).max); - - uint256 minAmountOut = daipsm.getRedeemAmountOut(mintAmount); - - vm.expectRevert("ERC20: transfer amount exceeds balance"); - migratorRouter.redeemUSDC(mintAmount, minAmountOut); - } - - function testRedeemDaiFailUnderfundedPSM() public { - _migratorSetup(); - - oldVolt.approve(address(migratorRouter), type(uint256).max); - - vm.prank(addresses.minterAddress); - volt.mint(address(voltMigrator), mintAmount); - - vm.prank(MainnetAddresses.GOVERNOR); - CoreV2(MainnetAddresses.CORE).grantMinter(address(this)); - oldVolt.mint(address(this), mintAmount); - - uint256 balance = dai.balanceOf(address(daipsm)); - vm.prank(address(daipsm)); - dai.transfer(address(0), balance); - - uint256 minAmountOut = daipsm.getRedeemAmountOut(mintAmount); - - vm.expectRevert("Dai/insufficient-balance"); - migratorRouter.redeemDai(mintAmount, minAmountOut); - } - - function testRedeemUsdcFailUnderfundedPSM() public { - _migratorSetup(); - - oldVolt.approve(address(migratorRouter), type(uint256).max); - - vm.prank(addresses.minterAddress); - volt.mint(address(voltMigrator), mintAmount); - - vm.prank(MainnetAddresses.GOVERNOR); - CoreV2(MainnetAddresses.CORE).grantMinter(address(this)); - oldVolt.mint(address(this), mintAmount); - - uint256 balance = usdc.balanceOf(address(usdcpsm)); - vm.prank(address(usdcpsm)); - usdc.transfer(address(1), balance); - - uint256 minAmountOut = usdcpsm.getRedeemAmountOut(mintAmount); - - vm.expectRevert("ERC20: transfer amount exceeds balance"); - migratorRouter.redeemUSDC(mintAmount, minAmountOut); - } - - function testRedeemDaiFailNoUserApproval() public { - _migratorSetup(); - - vm.prank(MainnetAddresses.GOVERNOR); - CoreV2(MainnetAddresses.CORE).grantMinter(address(this)); - oldVolt.mint(address(this), mintAmount); - - vm.prank(addresses.minterAddress); - volt.mint(address(voltMigrator), mintAmount); - - uint256 minAmountOut = daipsm.getRedeemAmountOut(mintAmount); - - vm.expectRevert("ERC20: transfer amount exceeds allowance"); - migratorRouter.redeemDai(mintAmount, minAmountOut); - } - - function testRedeemUsdcFailNoUserApproval() public { - _migratorSetup(); - - vm.prank(MainnetAddresses.GOVERNOR); - CoreV2(MainnetAddresses.CORE).grantMinter(address(this)); - oldVolt.mint(address(this), mintAmount); - - vm.prank(addresses.minterAddress); - volt.mint(address(voltMigrator), mintAmount); - - uint256 minAmountOut = daipsm.getRedeemAmountOut(mintAmount); - - vm.expectRevert("ERC20: transfer amount exceeds allowance"); - migratorRouter.redeemUSDC(mintAmount, minAmountOut); - } - - function testRedeemDaiFailUnderfundedMigrator() public { - _migratorSetup(); - - oldVolt.approve(address(migratorRouter), type(uint256).max); - - vm.prank(MainnetAddresses.GOVERNOR); - CoreV2(MainnetAddresses.CORE).grantMinter(address(this)); - oldVolt.mint(address(this), mintAmount); - - uint256 minAmountOut = usdcpsm.getRedeemAmountOut(mintAmount); - - vm.expectRevert(stdError.arithmeticError); - migratorRouter.redeemDai(mintAmount, minAmountOut); - } - - function testRedeemUsdcFailUnderfundedMigrator() public { - _migratorSetup(); - - oldVolt.approve(address(migratorRouter), type(uint256).max); - - vm.prank(MainnetAddresses.GOVERNOR); - CoreV2(MainnetAddresses.CORE).grantMinter(address(this)); - oldVolt.mint(address(this), mintAmount); - - uint256 minAmountOut = usdcpsm.getRedeemAmountOut(mintAmount); - - vm.expectRevert(stdError.arithmeticError); - migratorRouter.redeemUSDC(mintAmount, minAmountOut); - } -} From 55f86f1db23fed47351167fd63c0047b2fe1e3df Mon Sep 17 00:00:00 2001 From: Elliot Date: Wed, 18 Jan 2023 15:13:29 -0800 Subject: [PATCH 10/74] add _withdrawAndTransfer function --- contracts/pcv/PCVDepositV2.sol | 5 +++-- contracts/pcv/morpho/MorphoPCVDeposit.sol | 13 +++++-------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/contracts/pcv/PCVDepositV2.sol b/contracts/pcv/PCVDepositV2.sol index 5b3ab019a..7970d79b3 100644 --- a/contracts/pcv/PCVDepositV2.sol +++ b/contracts/pcv/PCVDepositV2.sol @@ -240,7 +240,8 @@ abstract contract PCVDepositV2 is IPCVDepositV2, CoreRefV2 { /// ------ Interactions ------ /// remove funds from underlying venue - _withdrawUnderlyingVenue(amount); + _withdrawAndTransfer(amount, to); + /// transfer funds to recipient IERC20(token).safeTransfer(to, amount); @@ -257,7 +258,7 @@ abstract contract PCVDepositV2 is IPCVDepositV2, CoreRefV2 { function _accrueUnderlying() internal virtual; /// @dev withdraw from the underlying market. - function _withdrawUnderlyingVenue(uint256 amount) internal virtual; + function _withdrawAndTransfer(uint256 amount, address to) internal virtual; /// @dev deposit in the underlying market. function _supply(uint256 amount) internal virtual; diff --git a/contracts/pcv/morpho/MorphoPCVDeposit.sol b/contracts/pcv/morpho/MorphoPCVDeposit.sol index 879ce7f15..c601f38ac 100644 --- a/contracts/pcv/morpho/MorphoPCVDeposit.sol +++ b/contracts/pcv/morpho/MorphoPCVDeposit.sol @@ -67,13 +67,6 @@ contract MorphoPCVDeposit is PCVDepositV2 { address _morpho, address _lens ) PCVDepositV2(_underlying, _rewardToken) CoreRefV2(_core) { - if (_underlying != address(Constants.WETH)) { - require( - ICToken(_cToken).underlying() == _underlying, - "MorphoPCVDeposit: Underlying mismatch" - ); - } - cToken = _cToken; morpho = _morpho; lens = _lens; @@ -110,8 +103,12 @@ contract MorphoPCVDeposit is PCVDepositV2 { } /// @dev withdraw from the underlying morpho market. - function _withdrawUnderlyingVenue(uint256 amount) internal override { + function _withdrawAndTransfer( + uint256 amount, + address to + ) internal override { IMorpho(morpho).withdraw(cToken, amount); + IERC20(token).safeTransfer(to, amount); } /// @dev deposit in the underlying morpho market. From 11e281a12cd0b0a1ce7c116c5db9fd26b97606ce Mon Sep 17 00:00:00 2001 From: Elliot Date: Wed, 18 Jan 2023 15:19:00 -0800 Subject: [PATCH 11/74] fix imports --- .../test/unit/pcv/compound/CompoundBadDebtSentinel.t.sol | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/contracts/test/unit/pcv/compound/CompoundBadDebtSentinel.t.sol b/contracts/test/unit/pcv/compound/CompoundBadDebtSentinel.t.sol index 848a7c419..8b99eebaa 100644 --- a/contracts/test/unit/pcv/compound/CompoundBadDebtSentinel.t.sol +++ b/contracts/test/unit/pcv/compound/CompoundBadDebtSentinel.t.sol @@ -17,7 +17,7 @@ import {GenericCallMock} from "../../../../mock/GenericCallMock.sol"; import {MockPCVDepositV3} from "../../../../mock/MockPCVDepositV3.sol"; import {MockERC20, IERC20} from "../../../../mock/MockERC20.sol"; import {CompoundBadDebtSentinel} from "../../../../pcv/compound/CompoundBadDebtSentinel.sol"; -import {MorphoCompoundPCVDeposit} from "../../../../pcv/morpho/MorphoCompoundPCVDeposit.sol"; +import {MorphoPCVDeposit} from "../../../../pcv/morpho/MorphoPCVDeposit.sol"; import {TestAddresses as addresses} from "../../utils/TestAddresses.sol"; import {MockMorphoMaliciousReentrancy} from "../../../../mock/MockMorphoMaliciousReentrancy.sol"; import {IGlobalReentrancyLock, GlobalReentrancyLock} from "../../../../core/GlobalReentrancyLock.sol"; @@ -41,7 +41,7 @@ contract UnitTestCompoundBadDebtSentinel is Test { PCVGuardian private pcvGuardian; GenericCallMock private comptroller; MockPCVDepositV3 private safeAddress; - MorphoCompoundPCVDeposit private morphoDeposit; + MorphoPCVDeposit private morphoDeposit; CompoundBadDebtSentinel private badDebtSentinel; uint256 public badDebtThreshold = 1_000_000e18; @@ -67,10 +67,11 @@ contract UnitTestCompoundBadDebtSentinel is Test { comptroller = new GenericCallMock(); safeAddress = new MockPCVDepositV3(address(core), address(token)); - morphoDeposit = new MorphoCompoundPCVDeposit( + morphoDeposit = new MorphoPCVDeposit( address(core), address(morpho), address(token), + address(0), address(morpho), address(morpho) ); From f2cb3a3515edd60aeea33d9dbd6fae95fb3ad790 Mon Sep 17 00:00:00 2001 From: Elliot Date: Wed, 18 Jan 2023 16:18:46 -0800 Subject: [PATCH 12/74] PCVDepositV2 _withdrawAndTransfer function --- contracts/pcv/PCVDepositV2.sol | 3 --- .../unit/pcv/morpho/MorphoCompoundPCVDeposit.t.sol | 14 -------------- 2 files changed, 17 deletions(-) diff --git a/contracts/pcv/PCVDepositV2.sol b/contracts/pcv/PCVDepositV2.sol index 39e919ef8..10c7e11b8 100644 --- a/contracts/pcv/PCVDepositV2.sol +++ b/contracts/pcv/PCVDepositV2.sol @@ -239,9 +239,6 @@ abstract contract PCVDepositV2 is IPCVDepositV2, CoreRefV2 { /// remove funds from underlying venue _withdrawAndTransfer(amount, to); - /// transfer funds to recipient - IERC20(token).safeTransfer(to, amount); - emit Withdrawal(msg.sender, to, amount); } diff --git a/contracts/test/unit/pcv/morpho/MorphoCompoundPCVDeposit.t.sol b/contracts/test/unit/pcv/morpho/MorphoCompoundPCVDeposit.t.sol index 423c614ec..d105ca478 100644 --- a/contracts/test/unit/pcv/morpho/MorphoCompoundPCVDeposit.t.sol +++ b/contracts/test/unit/pcv/morpho/MorphoCompoundPCVDeposit.t.sol @@ -101,20 +101,6 @@ contract UnitTestMorphoCompoundPCVDeposit is DSTest { assertEq(morphoDeposit.lastRecordedBalance(), 0); } - function testUnderlyingMismatchConstructionFails() public { - MockCToken cToken = new MockCToken(address(1)); - - vm.expectRevert("MorphoPCVDeposit: Underlying mismatch"); - new MorphoPCVDeposit( - address(core), - address(cToken), - address(token), - address(0), - address(morpho), - address(morpho) - ); - } - function testDeposit(uint120 depositAmount) public { assertEq(morphoDeposit.lastRecordedBalance(), 0); token.mint(address(morphoDeposit), depositAmount); From 492740285807f102d6afbea5c22ec7f3161ccb51 Mon Sep 17 00:00:00 2001 From: Elliot Date: Thu, 19 Jan 2023 01:27:47 -0800 Subject: [PATCH 13/74] Market governance --- contracts/pcv/IPCVDepositV2.sol | 2 + contracts/pcv/PCVRouter.sol | 5 + contracts/utils/Deviation.sol | 11 + contracts/vcon/MarketGovernanceVenue.sol | 304 +++++++++++++++++++++++ contracts/vcon/PCVBalance.sol | 3 + 5 files changed, 325 insertions(+) create mode 100644 contracts/vcon/MarketGovernanceVenue.sol create mode 100644 contracts/vcon/PCVBalance.sol diff --git a/contracts/pcv/IPCVDepositV2.sol b/contracts/pcv/IPCVDepositV2.sol index ce2e144fc..516525d53 100644 --- a/contracts/pcv/IPCVDepositV2.sol +++ b/contracts/pcv/IPCVDepositV2.sol @@ -11,4 +11,6 @@ interface IPCVDepositV2 is IPCVDeposit { function harvest() external; function accrue() external returns (uint256); + + function lastRecordedProfit() external returns (uint256); } diff --git a/contracts/pcv/PCVRouter.sol b/contracts/pcv/PCVRouter.sol index 912ba50a3..31f3813cb 100644 --- a/contracts/pcv/PCVRouter.sol +++ b/contracts/pcv/PCVRouter.sol @@ -116,6 +116,11 @@ contract PCVRouter is IPCVRouter, CoreRefV2 { IPCVDeposit(destination).balanceReportedIn() == destinationAsset, "PCVRouter: invalid destination asset" ); + + if (sourceAsset != destinationAsset) { + require(swapper != address(0), "MarketGovernance: invalid swapper"); + } + // Check swapper, if applicable if (swapper != address(0)) { require(isPCVSwapper(swapper), "PCVRouter: invalid swapper"); diff --git a/contracts/utils/Deviation.sol b/contracts/utils/Deviation.sol index 4776c2c97..839af3c17 100644 --- a/contracts/utils/Deviation.sol +++ b/contracts/utils/Deviation.sol @@ -49,6 +49,17 @@ library Deviation { return (basisPoints < 0 ? basisPoints * -1 : basisPoints).toUint256(); } + /// @notice return the percent deviation between a and b in basis points terms + function calculateDeviationBasisPoints( + int256 a, + int256 b + ) internal pure returns (int256) { + int256 delta = a - b; + int256 basisPoints = (delta * Constants.BP_INT) / a; + + return basisPoints; + } + /// @notice function to return whether or not the new price is within /// the acceptable deviation threshold function isWithinDeviationThreshold( diff --git a/contracts/vcon/MarketGovernanceVenue.sol b/contracts/vcon/MarketGovernanceVenue.sol new file mode 100644 index 000000000..654d4e2b8 --- /dev/null +++ b/contracts/vcon/MarketGovernanceVenue.sol @@ -0,0 +1,304 @@ +pragma solidity =0.8.13; + +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; + +import {Constants} from "../Constants.sol"; +import {PCVRouter} from "../pcv/PCVRouter.sol"; +import {CoreRefV2} from "../refs/CoreRefV2.sol"; +import {Deviation} from "../utils/Deviation.sol"; +import {IPCVDepositV2} from "../pcv/IPCVDepositV2.sol"; + +contract MarketGovernanceVenue is CoreRefV2 { + using Deviation for *; + using SafeERC20 for *; + using SafeCast for *; + + /// @notice reference to PCV Router + address public pcvRouter; + + /// @notice amount of VCON paid per unit of revenue generated + uint256 public profitToVconRatio; + + /// @notice total amount of VCON deposited across all venues + uint256 public totalSupply; + + /// TODO simplify this contract down to only track venue profit, + /// and do the conversion to VCON at the end when accruing rewards + + /// @notice last recorded profit index per venue + mapping(address => uint128) public lastRecordedProfit; + + /// @notice last recorded share price of a venue in VCON + /// starts off at 1e18 + mapping(address => uint128) public lastRecordedVconPricePerVenue; + + /// @notice total vcon deposited per venue + mapping(address => uint256) public totalVenueDepositedVcon; + + /// ---------- Per Venue User Profit Tracking ---------- + + /// @dev convention for all double nested address mappings is key (venue -> user) -> value + + /// @notice record of VCON index when user joined a given venue + mapping(address => mapping(address => uint256)) + public startingVenueVconProfit; + + /// @notice record how much VCON a user deposited in a given venue + mapping(address => mapping(address => uint256)) + public userVenueDepositedVcon; + + /// ---------- Per Venue User Profit Tracking ---------- + + /// @notice approved routes to swap different tokens and their corresponding swapper address + /// first address is token from, second address is tokenTo, third address is corresponding swapper + /// the starting approved routes will be DAI -> USDC and USDC -> DAI through the Maker PCV Swapper + mapping(address => mapping(address => address)) public approvedRoutes; + + /// @param _core reference to core + /// @param _pcvRouter reference to the PCV Router + /// @param _profitToVconRatio ratio of VCON paid out per dollar in revenue + constructor( + address _core, + address _pcvRouter, + uint256 _profitToVconRatio + ) CoreRefV2(_core) { + pcvRouter = _pcvRouter; + profitToVconRatio = _profitToVconRatio; + } + + /// TODO update pcv oracle to cache total pcv so getAllPCV isn't needed to figure + /// out if weights are correct + /// TODO change movePCV to use a different global reentrancy lock state, or require system be lock level 1 to use that function + /// @param source address to pull funds from + /// @param destination address to accrue rewards to, and send funds to + function deposit( + uint256 amountVcon, + uint256 amountPcv, + address source, + address destination + ) external globalLock(1) { + _accrue(destination); /// update profitPerVCON in the destination so the user gets in at the current share price + uint256 vconRewards = _harvestRewards(msg.sender, destination); /// auto-compound rewards + /// check and an interaction with a trusted contract + vcon().safeTransferFrom(msg.sender, address(this), amountVcon); /// transfer VCON in + + uint256 totalVconDeposited = amountVcon + vconRewards; /// auto-compound rewards + + /// global updates + totalSupply += totalVconDeposited; + + /// user updates + userVenueDepositedVcon[destination][msg.sender] += totalVconDeposited; + + /// venue updates + totalVenueDepositedVcon[destination] += totalVconDeposited; + + _movePcv(source, destination, amountPcv); + } + + /// @notice unstake VCON and transfer corresponding VCON to another venue + /// @param amountVcon the amount of VCON staked to unstake + /// @param source address to accrue rewards to, and pull funds from + function withdraw( + uint256 amountVcon, + uint256 amountPcv, + address source, + address destination, + address vconRecipient + ) external globalLock(1) { + require(source != destination, "MarketGovernance: src and dest equal"); + + _accrue(source); /// update profitPerVCON in the source so the user gets paid out at the current share price + { + uint256 vconRewards = _harvestRewards(msg.sender, source); /// pay msg.sender their rewards + + require( + userVenueDepositedVcon[source][msg.sender] + vconRewards >= + amountVcon, + "MarketGovernance: invalid vcon amount" + ); + + /// check and an interaction with a trusted contract + + vcon().safeTransfer(vconRecipient, amountVcon); /// transfer VCON to recipient + + /// global updates + totalSupply -= amountVcon; + + /// user updates + /// balance = 80 + /// amount = 100 + /// rewards = 30 + /// amount deducted = 70 + /// _____________ + /// balance = 10 + userVenueDepositedVcon[source][msg.sender] -= + amountVcon - + vconRewards; + } + + /// venue updates + totalVenueDepositedVcon[destination] -= amountVcon; + + _movePcv(source, destination, amountPcv); + } + + /// @notice rebalance PCV without staking or unstaking VCON + function rebalance( + address source, + address destination, + uint256 amountPcv + ) external globalLock(1) { + _movePcv(source, destination, amountPcv); + } + + function _movePcv( + address source, + address destination, + uint256 amountPcv + ) private { + address sourceAsset = IPCVDepositV2(source).balanceReportedIn(); + address destinationAsset = IPCVDepositV2(destination) + .balanceReportedIn(); + address swapper = approvedRoutes[sourceAsset][destinationAsset]; + + /// TODO fix this method on the oracle because it doesn't allow the read while in lock mode + uint256 totalPcv = pcvOracle().getTotalPcv(); + + /// record how balanced the system is before the PCV movement + int256 sourceVenueBalance = getVenueBalance(source, totalPcv); + int256 destinationVenueBalance = getVenueBalance(destination, totalPcv); + + /// optimistically transfer funds to the specified pcv deposit + /// swapper validity not checked in this contract as the PCV Router will check this + PCVRouter(pcvRouter).movePCV( + source, + destination, + swapper, + amountPcv, + sourceAsset, + destinationAsset + ); + + /// TODO simplify this by passing delta of starting balance instead of calling balance again on each pcv deposit + /// record how balanced the system is before the PCV movement + int256 sourceVenueBalanceAfter = getVenueBalance(source, totalPcv); + int256 destinationVenueBalanceAfter = getVenueBalance( + destination, + totalPcv + ); + + /// source and dest venue balance measures the distance from being perfectly balanced + /// validate source venue balance became more balanced + require( + sourceVenueBalance < 0 + ? /// if balance is under weight relative to vcon staked, ensure it doesn't go over balance + sourceVenueBalanceAfter > sourceVenueBalance && + sourceVenueBalanceAfter <= 0 + : /// if balance is over weight relative to vcon staked, ensure it doesn't go under balance + sourceVenueBalanceAfter < sourceVenueBalance && + sourceVenueBalanceAfter >= 0, + "MarketGovernance: src more imbalanced" + ); + + /// validate destination venue balance became more balanced + require( + destinationVenueBalance < 0 + ? /// if balance is under weight relative to vcon staked, ensure it doesn't go over balance + destinationVenueBalanceAfter > destinationVenueBalance && + destinationVenueBalanceAfter <= 0 + : /// if balance is over weight relative to vcon staked, ensure it doesn't go under balance + destinationVenueBalanceAfter > destinationVenueBalance && + destinationVenueBalanceAfter >= 0, + "MarketGovernance: src more imbalanced" + ); + } + + /// @notice returns profit in VCON a user has accrued + /// does not update how much VCON a user has staked to save on gas + /// that updating happens in the calling function + function _harvestRewards( + address user, + address venue + ) private returns (uint256) { + uint256 vconStartIndex = startingVenueVconProfit[venue][user]; + uint256 vconCurrentIndex = lastRecordedVconPricePerVenue[venue]; + + /// do not pay out if user has not entered the market + if (vconStartIndex == 0) { + /// set user starting profit to current venue profit index + startingVenueVconProfit[venue][user] = vconCurrentIndex; + return 0; /// no profits + } + + uint256 vconRewards = (vconCurrentIndex - vconStartIndex) * + userVenueDepositedVcon[venue][user]; + startingVenueVconProfit[venue][user] = vconCurrentIndex; + + return vconRewards; + } + + function _accrue(address venue) private { + uint256 startingLastRecordedProfit = lastRecordedProfit[venue]; + + IPCVDepositV2(venue).accrue(); + + uint256 endingLastRecordedProfit = IPCVDepositV2(venue) + .lastRecordedProfit(); + int256 deltaProfit = endingLastRecordedProfit.toInt256() - + startingLastRecordedProfit.toInt256(); /// also could be a loss + + /// amount of VCON that each VCON deposit receives for being in this venue + int256 profitPerVconDelta = (deltaProfit * + profitToVconRatio.toInt256()) / totalSupply.toInt256(); + + lastRecordedVconPricePerVenue[venue] = (lastRecordedVconPricePerVenue[ + venue + ].toInt256() + profitPerVconDelta).toUint256().toUint128(); + } + + /// @notice returns positive value if over allocated + /// returns negative value if under allocated + function getVenueBalance( + address venue, + uint256 totalPcv + ) public view returns (int256) { + uint256 venueDepositedVcon = totalVenueDepositedVcon[venue]; + + uint256 venueBalance = IPCVDepositV2(venue).balance(); + + if (venueDepositedVcon == 0 && venueBalance == 0) { + return 0; /// perfectly balanced at 0 PCV + } + + /// if no venue balance and deposited vcon, return positive + /// if venue balance and no deposited vcon, return negative + + /// find out the actual ratio of PCV in a given venue based on PCV + uint256 venueRatio = (venueBalance * Constants.ETH_GRANULARITY) / + totalPcv; + + /// find out expected ratio of PCV in a given venue based on VCON staked + uint256 venueDepositedVconRatio = (venueDepositedVcon * + Constants.ETH_GRANULARITY) / totalSupply; + + /// if no venue deposited vcon, we would divide by 0, and revert, so reverse order and multiply by -1 + /// this means there is too much PCV for the amount of VCON in the venue + if (venueDepositedVconRatio == 0) { + return + -1 * + Deviation.calculateDeviationBasisPoints( + venueRatio.toInt256(), + venueDepositedVconRatio.toInt256() + ); + } + + /// if venue deposited VCON, return the regular ratio between vcon deposited and pcv deposited + return + Deviation.calculateDeviationBasisPoints( + venueDepositedVconRatio.toInt256(), + venueRatio.toInt256() + ); + } +} diff --git a/contracts/vcon/PCVBalance.sol b/contracts/vcon/PCVBalance.sol new file mode 100644 index 000000000..fc10eeaa1 --- /dev/null +++ b/contracts/vcon/PCVBalance.sol @@ -0,0 +1,3 @@ +pragma solidity =0.8.13; + +contract PCVBalance {} From 77f7223cfaee27e3478fb2f0a5b236796953eb6d Mon Sep 17 00:00:00 2001 From: Elliot Date: Thu, 19 Jan 2023 12:44:41 -0800 Subject: [PATCH 14/74] Market Governance compatability and decimal normalization for PCV Oracle --- contracts/Constants.sol | 3 +++ contracts/oracle/IPCVOracle.sol | 3 +++ contracts/oracle/PCVOracle.sol | 32 ++++++++++++++++++++---- contracts/vcon/MarketGovernanceVenue.sol | 27 ++++++++++---------- 4 files changed, 47 insertions(+), 18 deletions(-) diff --git a/contracts/Constants.sol b/contracts/Constants.sol index 20e4994d5..77741305e 100644 --- a/contracts/Constants.sol +++ b/contracts/Constants.sol @@ -28,6 +28,9 @@ library Constants { /// @notice Wei per ETH, i.e. 10**18 uint256 public constant ETH_GRANULARITY = 1e18; + /// @notice Wei per ETH, i.e. 10**18 + int256 public constant ETH_GRANULARITY_INT = 1e18; + /// @notice number of decimals in ETH, 18 uint256 public constant ETH_DECIMALS = 18; } diff --git a/contracts/oracle/IPCVOracle.sol b/contracts/oracle/IPCVOracle.sol index 5f4caa116..5299c584b 100644 --- a/contracts/oracle/IPCVOracle.sol +++ b/contracts/oracle/IPCVOracle.sol @@ -43,6 +43,9 @@ interface IPCVOracle { /// @dev this function is meant to be used offchain, as it is pretty gas expensive. function getTotalPcv() external view returns (uint256 totalPcv); + /// @notice returns decimal normalized version of a given venues USD balance + function getVenueBalance(address venue) external view returns (uint256); + // ----------- PCVDeposit-only State changing API -------------- /// @notice hook on PCV deposit, callable when pcv oracle is set diff --git a/contracts/oracle/PCVOracle.sol b/contracts/oracle/PCVOracle.sol index 35c9a0568..67fd7110e 100644 --- a/contracts/oracle/PCVOracle.sol +++ b/contracts/oracle/PCVOracle.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.13; import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import {Constants} from "../Constants.sol"; import {IOracleV2} from "./IOracleV2.sol"; import {IPCVOracle} from "./IPCVOracle.sol"; import {VoltRoles} from "../core/VoltRoles.sol"; @@ -68,11 +69,28 @@ contract PCVOracle is IPCVOracle, CoreRefV2 { require(oracleValid, "PCVOracle: invalid oracle value"); uint256 balance = IPCVDepositV2(depositAddress).balance(); - totalPcv += (oracleValue * balance) / 1e18; + totalPcv += (oracleValue * balance) / Constants.ETH_GRANULARITY; } } } + /// @notice returns decimal normalized version of a given venues USD balance + function getVenueBalance( + address venue + ) external view override returns (uint256) { + // Read oracle to get USD values of delta + address oracle = venueToOracle[venue]; + + require(oracle != address(0), "PCVOracle: invalid caller deposit"); + (uint256 oracleValue, bool oracleValid) = IOracleV2(oracle).read(); + + require(oracleValid, "PCVOracle: invalid oracle value"); + uint256 venueBalance = IPCVDepositV2(venue).balance(); + + // Compute USD values of deposit + return (oracleValue * venueBalance) / Constants.ETH_GRANULARITY; + } + /// ------------- PCV Deposit Only API ------------- /// @notice only callable by a pcv deposit that has previously been listed @@ -124,8 +142,10 @@ contract PCVOracle is IPCVOracle, CoreRefV2 { uint256 venueBalance = IPCVDepositV2(venue).accrue(); if (venueBalance != 0) { // Compute balance diff - uint256 oldBalanceUSD = (venueBalance * oldOracleValue) / 1e18; - uint256 newBalanceUSD = (venueBalance * newOracleValue) / 1e18; + uint256 oldBalanceUSD = (venueBalance * oldOracleValue) / + Constants.ETH_GRANULARITY; + uint256 newBalanceUSD = (venueBalance * newOracleValue) / + Constants.ETH_GRANULARITY; int256 deltaBalanceUSD = int256(newBalanceUSD) - int256(oldBalanceUSD); @@ -225,8 +245,10 @@ contract PCVOracle is IPCVOracle, CoreRefV2 { (uint256 oracleValue, bool oracleValid) = IOracleV2(oracle).read(); require(oracleValid, "PCVOracle: invalid oracle value"); // Compute USD values of delta - int256 deltaBalanceUSD = (int256(oracleValue) * deltaBalance) / 1e18; - int256 deltaProfitUSD = (int256(oracleValue) * deltaProfit) / 1e18; + int256 deltaBalanceUSD = (int256(oracleValue) * deltaBalance) / + Constants.ETH_GRANULARITY_INT; + int256 deltaProfitUSD = (int256(oracleValue) * deltaProfit) / + Constants.ETH_GRANULARITY_INT; _updateAccounting(venue, deltaBalanceUSD, deltaProfitUSD); } diff --git a/contracts/vcon/MarketGovernanceVenue.sol b/contracts/vcon/MarketGovernanceVenue.sol index 654d4e2b8..20fe43bc7 100644 --- a/contracts/vcon/MarketGovernanceVenue.sol +++ b/contracts/vcon/MarketGovernanceVenue.sol @@ -192,24 +192,20 @@ contract MarketGovernanceVenue is CoreRefV2 { /// source and dest venue balance measures the distance from being perfectly balanced /// validate source venue balance became more balanced require( - sourceVenueBalance < 0 - ? /// if balance is under weight relative to vcon staked, ensure it doesn't go over balance - sourceVenueBalanceAfter > sourceVenueBalance && - sourceVenueBalanceAfter <= 0 - : /// if balance is over weight relative to vcon staked, ensure it doesn't go under balance - sourceVenueBalanceAfter < sourceVenueBalance && + sourceVenueBalance < 0 /// if balance is under weight relative to vcon staked, ensure it doesn't go over balance + ? sourceVenueBalanceAfter > sourceVenueBalance && + sourceVenueBalanceAfter <= 0 /// if balance is over weight relative to vcon staked, ensure it doesn't go under balance + : sourceVenueBalanceAfter < sourceVenueBalance && sourceVenueBalanceAfter >= 0, "MarketGovernance: src more imbalanced" ); /// validate destination venue balance became more balanced require( - destinationVenueBalance < 0 - ? /// if balance is under weight relative to vcon staked, ensure it doesn't go over balance - destinationVenueBalanceAfter > destinationVenueBalance && - destinationVenueBalanceAfter <= 0 - : /// if balance is over weight relative to vcon staked, ensure it doesn't go under balance - destinationVenueBalanceAfter > destinationVenueBalance && + destinationVenueBalance < 0 /// if balance is under weight relative to vcon staked, ensure it doesn't go over balance + ? destinationVenueBalanceAfter > destinationVenueBalance && + destinationVenueBalanceAfter <= 0 /// if balance is over weight relative to vcon staked, ensure it doesn't go under balance + : destinationVenueBalanceAfter > destinationVenueBalance && destinationVenueBalanceAfter >= 0, "MarketGovernance: src more imbalanced" ); @@ -266,7 +262,7 @@ contract MarketGovernanceVenue is CoreRefV2 { ) public view returns (int256) { uint256 venueDepositedVcon = totalVenueDepositedVcon[venue]; - uint256 venueBalance = IPCVDepositV2(venue).balance(); + uint256 venueBalance = pcvOracle().getVenueBalance(venue); if (venueDepositedVcon == 0 && venueBalance == 0) { return 0; /// perfectly balanced at 0 PCV @@ -301,4 +297,9 @@ contract MarketGovernanceVenue is CoreRefV2 { venueRatio.toInt256() ); } + + /// TODO add governance API's to change the pcv router + /// profitToVconRatio + /// approved routes + /// TODO add events } From b8ffb03cf09035609e8ad2c3ea60210cd74ca133 Mon Sep 17 00:00:00 2001 From: Elliot Date: Thu, 19 Jan 2023 13:39:53 -0800 Subject: [PATCH 15/74] remove contracts folder --- .../IntegrationTestERC20Allocator.t.sol | 338 ------------------ .../IntegrationTestCompoundPCVRouter.t.sol | 210 ----------- contracts/test/integration/vip/vip14.sol | 300 ---------------- 3 files changed, 848 deletions(-) delete mode 100644 contracts/test/integration/IntegrationTestERC20Allocator.t.sol delete mode 100644 contracts/test/integration/compound/IntegrationTestCompoundPCVRouter.t.sol delete mode 100644 contracts/test/integration/vip/vip14.sol diff --git a/contracts/test/integration/IntegrationTestERC20Allocator.t.sol b/contracts/test/integration/IntegrationTestERC20Allocator.t.sol deleted file mode 100644 index 32c2d9e78..000000000 --- a/contracts/test/integration/IntegrationTestERC20Allocator.t.sol +++ /dev/null @@ -1,338 +0,0 @@ -//SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity =0.8.13; - -import {TimelockController} from "@openzeppelin/contracts/governance/TimelockController.sol"; -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {Vm} from "../unit/utils/Vm.sol"; -import {Core} from "../../core/Core.sol"; -import {vip10} from "./vip/vip10.sol"; -import {IVolt} from "../../volt/IVolt.sol"; -import {DSTest} from "../unit/utils/DSTest.sol"; -import {IDSSPSM} from "../../pcv/maker/IDSSPSM.sol"; -import {Constants} from "../../Constants.sol"; -import {PCVDeposit} from "../../pcv/PCVDeposit.sol"; -import {PCVGuardian} from "../../pcv/PCVGuardian.sol"; -import {ERC20Allocator} from "../../pcv/utils/ERC20Allocator.sol"; -import {MainnetAddresses} from "./fixtures/MainnetAddresses.sol"; -import {TimelockSimulation} from "./utils/TimelockSimulation.sol"; -import {PegStabilityModule} from "../../peg/PegStabilityModule.sol"; -import {MorphoPCVDeposit} from "../../pcv/morpho/MorphoPCVDeposit.sol"; - -contract IntegrationTestERC20Allocator is DSTest { - using SafeCast for *; - - Vm public constant vm = Vm(HEVM_ADDRESS); - - PCVGuardian private immutable mainnetPCVGuardian = - PCVGuardian(MainnetAddresses.PCV_GUARDIAN); - MorphoPCVDeposit private daiDeposit = - MorphoPCVDeposit(MainnetAddresses.MORPHO_COMPOUND_DAI_PCV_DEPOSIT); - MorphoPCVDeposit private usdcDeposit = - MorphoPCVDeposit(MainnetAddresses.MORPHO_COMPOUND_USDC_PCV_DEPOSIT); - - PCVGuardian private immutable pcvGuardian = - PCVGuardian(MainnetAddresses.PCV_GUARDIAN); - - Core private core = Core(MainnetAddresses.CORE); - PegStabilityModule private daiPSM = - PegStabilityModule(MainnetAddresses.VOLT_DAI_PSM); - PegStabilityModule private usdcPSM = - PegStabilityModule(MainnetAddresses.VOLT_USDC_PSM); - - IERC20 private dai = IERC20(MainnetAddresses.DAI); - IVolt private fei = IVolt(MainnetAddresses.FEI); - IERC20 private usdc = IERC20(MainnetAddresses.USDC); - - ERC20Allocator private allocator = - ERC20Allocator(MainnetAddresses.ERC20ALLOCATOR); - - uint256 public constant scalingFactorUsdc = 1e12; - int8 public constant decimalsNormalizerUsdc = 12; - int8 public constant decimalsNormalizerDai = 0; - - uint248 public constant targetUsdcBalance = 100_000e6; - uint248 public constant targetDaiBalance = 100_000e18; - - uint256 public constant maxRateLimitPerSecond = 1_000e18; /// 1k volt per second - /// @notice rate limit per second is designed to allow system to replenish - /// full buffercap every 24 hours assuming drip only - /// 500,000 / 86,400 = 5.787 VOLT per second - uint128 public constant rateLimitPerSecond = 5.78e18; - uint128 public constant bufferCap = 300_000e18; - - function setUp() public { - uint256 usdcBalance = usdc.balanceOf( - MainnetAddresses.KRAKEN_USDC_WHALE - ); - vm.prank(MainnetAddresses.KRAKEN_USDC_WHALE); - usdc.transfer(address(usdcDeposit), usdcBalance); - usdcDeposit.deposit(); - } - - function testSetup() public { - assertEq(address(allocator.core()), address(core)); - - { - ( - address psmToken, - uint248 psmTargetBalance, - int8 decimalsNormalizer - ) = allocator.allPSMs(address(daiPSM)); - address daipsm = allocator.pcvDepositToPSM(address(daiDeposit)); - - assertEq(psmTargetBalance, targetDaiBalance); - assertEq(decimalsNormalizer, decimalsNormalizerDai); - assertEq(psmToken, address(dai)); - assertEq(daipsm, address(daiPSM)); - } - - { - ( - address psmToken, - uint248 psmTargetBalance, - int8 decimalsNormalizer - ) = allocator.allPSMs(address(usdcPSM)); - address usdcpsm = allocator.pcvDepositToPSM(address(usdcDeposit)); - - assertEq(psmTargetBalance, targetUsdcBalance); - assertEq(decimalsNormalizer, decimalsNormalizerUsdc); - assertEq(psmToken, address(usdc)); - assertEq(usdcpsm, address(usdcPSM)); - } - } - - /// ------ DRIP ------ - - function testDripDai() public { - uint256 daiBalance = dai.balanceOf(address(daiPSM)); - - vm.prank(MainnetAddresses.GOVERNOR); - daiPSM.withdraw(address(daiDeposit), daiBalance); /// send all dai to pcv deposit - daiDeposit.deposit(); - - (uint256 amountToDrip, uint256 adjustedAmountToDrip) = allocator - .getDripDetails(address(daiPSM), address(daiDeposit)); - - assertTrue(allocator.checkDripCondition(address(daiDeposit))); - assertTrue(!allocator.checkSkimCondition(address(daiDeposit))); - assertTrue(allocator.checkActionAllowed(address(daiDeposit))); - allocator.drip(address(daiDeposit)); - assertTrue(!allocator.checkDripCondition(address(daiDeposit))); - assertTrue(!allocator.checkSkimCondition(address(daiDeposit))); - assertTrue(!allocator.checkActionAllowed(address(daiDeposit))); - - daiBalance = dai.balanceOf(address(daiPSM)); - - assertEq(amountToDrip, adjustedAmountToDrip); - assertEq(amountToDrip, targetDaiBalance); - assertEq(daiBalance, targetDaiBalance); - } - - function testDripUsdc() public { - uint256 usdcBalance = usdc.balanceOf(address(usdcPSM)); - - vm.prank(MainnetAddresses.GOVERNOR); - usdcPSM.withdraw(address(usdcDeposit), usdcBalance); /// send all dai to pcv deposit - usdcDeposit.deposit(); /// deposit so it will be counted in balance - - (uint256 amountToDrip, uint256 adjustedAmountToDrip) = allocator - .getDripDetails(address(usdcPSM), address(usdcDeposit)); - - assertTrue(allocator.checkDripCondition(address(usdcDeposit))); - assertTrue(allocator.checkActionAllowed(address(usdcDeposit))); - allocator.drip(address(usdcDeposit)); - assertTrue(!allocator.checkDripCondition(address(usdcDeposit))); - assertTrue(!allocator.checkActionAllowed(address(usdcDeposit))); - - usdcBalance = usdc.balanceOf(address(usdcPSM)); - assertEq(amountToDrip * scalingFactorUsdc, adjustedAmountToDrip); - assertEq(amountToDrip, targetUsdcBalance); - assertEq(usdcBalance, targetUsdcBalance); - } - - /// ------ SKIM ------ - - function testSkimDai() public { - uint256 daiBalance = daiDeposit.balance(); - uint256 daiPSMBalance = daiPSM.balance(); - - vm.prank(MainnetAddresses.GOVERNOR); - daiDeposit.withdraw(address(daiPSM), daiBalance); /// send all dai to psm - - (uint256 amountToSkim, uint256 adjustedAmountToSkim) = allocator - .getSkimDetails(address(daiDeposit)); - - uint256 skimAmount = daiPSM.balance() - targetDaiBalance; - assertEq(amountToSkim, skimAmount); - assertEq(adjustedAmountToSkim, skimAmount); - - assertTrue(!allocator.checkDripCondition(address(daiDeposit))); /// all assets are in dai psm, not eligble for skim - assertTrue(allocator.checkSkimCondition(address(daiDeposit))); - assertTrue(allocator.checkActionAllowed(address(daiDeposit))); - - allocator.skim(address(daiDeposit)); - - assertTrue(!allocator.checkDripCondition(address(daiDeposit))); - assertTrue(!allocator.checkSkimCondition(address(daiDeposit))); - assertTrue(!allocator.checkActionAllowed(address(daiDeposit))); - - assertEq(dai.balanceOf(address(daiPSM)), targetDaiBalance); - assertApproxEq( - daiDeposit.balance().toInt256(), - (daiBalance + daiPSMBalance - targetDaiBalance).toInt256(), - 0 - ); - } - - function testSkimUsdc() public { - uint256 usdcBalance = usdcDeposit.balance(); - uint256 usdcPSMBalance = usdcPSM.balance(); - - vm.prank(MainnetAddresses.GOVERNOR); - usdcDeposit.withdraw(address(usdcPSM), usdcBalance); /// send all usdc to psm - - (uint256 amountToSkim, uint256 adjustedAmountToSkim) = allocator - .getSkimDetails(address(usdcDeposit)); - - uint256 skimAmount = usdcPSM.balance() - targetUsdcBalance; - assertEq(amountToSkim, skimAmount); - assertEq(adjustedAmountToSkim, skimAmount * scalingFactorUsdc); - - assertTrue(!allocator.checkDripCondition(address(usdcDeposit))); /// all assets are in usdc psm, not eligble for skim - assertTrue(allocator.checkSkimCondition(address(usdcDeposit))); - assertTrue(allocator.checkActionAllowed(address(usdcDeposit))); - - allocator.skim(address(usdcDeposit)); - - assertTrue(!allocator.checkDripCondition(address(usdcDeposit))); - assertTrue(!allocator.checkSkimCondition(address(usdcDeposit))); - assertTrue(!allocator.checkActionAllowed(address(usdcDeposit))); - - assertEq(usdc.balanceOf(address(usdcPSM)), targetUsdcBalance); - assertApproxEq( - usdcDeposit.balance().toInt256(), - (usdcBalance + usdcPSMBalance - targetUsdcBalance).toInt256(), - 0 - ); - } - - /// ------ DRIP ------ - - function testDripDaiViaDoAction() public { - uint256 daiBalance = dai.balanceOf(address(daiPSM)); - - vm.prank(MainnetAddresses.GOVERNOR); - daiPSM.withdraw(address(daiDeposit), daiBalance); /// send all dai to pcv deposit - daiDeposit.deposit(); - - (uint256 amountToDrip, uint256 adjustedAmountToDrip) = allocator - .getDripDetails(address(daiPSM), address(daiDeposit)); - - assertTrue(allocator.checkDripCondition(address(daiDeposit))); - assertTrue(!allocator.checkSkimCondition(address(daiDeposit))); - assertTrue(allocator.checkActionAllowed(address(daiDeposit))); - - allocator.doAction(address(daiDeposit)); - - assertTrue(!allocator.checkDripCondition(address(daiDeposit))); - assertTrue(!allocator.checkSkimCondition(address(daiDeposit))); - assertTrue(!allocator.checkActionAllowed(address(daiDeposit))); - - daiBalance = dai.balanceOf(address(daiPSM)); - - assertEq(amountToDrip, adjustedAmountToDrip); - assertEq(amountToDrip, targetDaiBalance); - assertEq(daiBalance, targetDaiBalance); - } - - function testDripUsdcViaDoAction() public { - uint256 usdcBalance = usdc.balanceOf(address(usdcPSM)); - - vm.prank(MainnetAddresses.GOVERNOR); - usdcPSM.withdraw(address(usdcDeposit), usdcBalance); /// send all dai to pcv deposit - usdcDeposit.deposit(); /// deposit so it will be counted in balance - - (uint256 amountToDrip, uint256 adjustedAmountToDrip) = allocator - .getDripDetails(address(usdcPSM), address(usdcDeposit)); - - assertTrue(allocator.checkDripCondition(address(usdcDeposit))); - assertTrue(allocator.checkActionAllowed(address(usdcDeposit))); - - allocator.doAction(address(usdcDeposit)); - - assertTrue(!allocator.checkDripCondition(address(usdcDeposit))); - assertTrue(!allocator.checkActionAllowed(address(usdcDeposit))); - - usdcBalance = usdc.balanceOf(address(usdcPSM)); - assertEq(amountToDrip * scalingFactorUsdc, adjustedAmountToDrip); - assertEq(amountToDrip, targetUsdcBalance); - assertEq(usdcBalance, targetUsdcBalance); - } - - /// ------ SKIM ------ - - function testSkimDaiViaDoAction() public { - uint256 daiBalance = daiDeposit.balance(); - uint256 daiPSMBalance = daiPSM.balance(); - - vm.prank(MainnetAddresses.GOVERNOR); - daiDeposit.withdraw(address(daiPSM), daiBalance); /// send all dai to psm - - (uint256 amountToSkim, uint256 adjustedAmountToSkim) = allocator - .getSkimDetails(address(daiDeposit)); - - uint256 skimAmount = daiPSM.balance() - targetDaiBalance; - assertEq(amountToSkim, skimAmount); - assertEq(adjustedAmountToSkim, skimAmount); - - assertTrue(!allocator.checkDripCondition(address(daiDeposit))); /// all assets are in dai psm, not eligble for skim - assertTrue(allocator.checkSkimCondition(address(daiDeposit))); - assertTrue(allocator.checkActionAllowed(address(daiDeposit))); - - allocator.doAction(address(daiDeposit)); - - assertTrue(!allocator.checkDripCondition(address(daiDeposit))); - assertTrue(!allocator.checkSkimCondition(address(daiDeposit))); - assertTrue(!allocator.checkActionAllowed(address(daiDeposit))); - - assertEq(dai.balanceOf(address(daiPSM)), targetDaiBalance); - assertApproxEq( - daiDeposit.balance().toInt256(), - (daiBalance + daiPSMBalance - targetDaiBalance).toInt256(), - 0 - ); - } - - function testSkimUsdcViaDoAction() public { - uint256 usdcBalance = usdcDeposit.balance(); - uint256 usdcPSMBalance = usdcPSM.balance(); - - vm.prank(MainnetAddresses.GOVERNOR); - usdcDeposit.withdraw(address(usdcPSM), usdcBalance); /// send all usdc to psm - - (uint256 amountToSkim, uint256 adjustedAmountToSkim) = allocator - .getSkimDetails(address(usdcDeposit)); - - uint256 skimAmount = usdcPSM.balance() - targetUsdcBalance; - assertEq(amountToSkim, skimAmount); - assertEq(adjustedAmountToSkim, skimAmount * scalingFactorUsdc); - - assertTrue(!allocator.checkDripCondition(address(usdcDeposit))); /// all assets are in usdc psm, not eligble for skim - assertTrue(allocator.checkSkimCondition(address(usdcDeposit))); - assertTrue(allocator.checkActionAllowed(address(usdcDeposit))); - - allocator.doAction(address(usdcDeposit)); - - assertTrue(!allocator.checkDripCondition(address(usdcDeposit))); - assertTrue(!allocator.checkSkimCondition(address(usdcDeposit))); - assertTrue(!allocator.checkActionAllowed(address(usdcDeposit))); - - assertEq(usdc.balanceOf(address(usdcPSM)), targetUsdcBalance); - assertApproxEq( - usdcDeposit.balance().toInt256(), - (usdcBalance + usdcPSMBalance - targetUsdcBalance).toInt256(), - 0 - ); - } -} diff --git a/contracts/test/integration/compound/IntegrationTestCompoundPCVRouter.t.sol b/contracts/test/integration/compound/IntegrationTestCompoundPCVRouter.t.sol deleted file mode 100644 index 6bfc32360..000000000 --- a/contracts/test/integration/compound/IntegrationTestCompoundPCVRouter.t.sol +++ /dev/null @@ -1,210 +0,0 @@ -pragma solidity 0.8.13; - -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; - -import {Vm} from "../../unit/utils/Vm.sol"; -import {Core} from "../../../core/Core.sol"; -import {CToken} from "../../../pcv/compound/CToken.sol"; -import {DSTest} from "../../unit/utils/DSTest.sol"; -import {IDSSPSM} from "./../../../pcv/maker/IDSSPSM.sol"; -import {stdError} from "../../unit/utils/StdLib.sol"; -import {MockERC20} from "../../../mock/MockERC20.sol"; -import {VoltRoles} from "../../../core/VoltRoles.sol"; -import {PCVGuardian} from "../../../pcv/PCVGuardian.sol"; -import {MainnetAddresses} from "../fixtures/MainnetAddresses.sol"; -import {ArbitrumAddresses} from "../fixtures/ArbitrumAddresses.sol"; -import {CompoundPCVRouter} from "../../../pcv/compound/CompoundPCVRouter.sol"; -import {MorphoPCVDeposit} from "../../../pcv/morpho/MorphoPCVDeposit.sol"; - -contract IntegrationTestCompoundPCVRouter is DSTest { - using SafeCast for *; - - Vm public constant vm = Vm(HEVM_ADDRESS); - - Core private core = Core(MainnetAddresses.CORE); - IERC20 private usdc = IERC20(MainnetAddresses.USDC); - IERC20 private dai = IERC20(MainnetAddresses.DAI); - address private governor = MainnetAddresses.GOVERNOR; - - CToken private cDai = CToken(MainnetAddresses.CDAI); - CToken private cUsdc = CToken(MainnetAddresses.CUSDC); - - IDSSPSM public immutable daiPSM = - IDSSPSM(0x89B78CfA322F6C5dE0aBcEecab66Aee45393cC5A); - - CompoundPCVRouter private compoundRouter = - CompoundPCVRouter(MainnetAddresses.MORPHO_COMPOUND_PCV_ROUTER); - - MorphoPCVDeposit private daiDeposit = - MorphoPCVDeposit(MainnetAddresses.MORPHO_COMPOUND_DAI_PCV_DEPOSIT); - MorphoPCVDeposit private usdcDeposit = - MorphoPCVDeposit(MainnetAddresses.MORPHO_COMPOUND_USDC_PCV_DEPOSIT); - - address public immutable pcvGuard = MainnetAddresses.EOA_1; - PCVGuardian public immutable pcvGuardian = - PCVGuardian(MainnetAddresses.PCV_GUARDIAN); - - address public constant makerWard = - 0xBE8E3e3618f7474F8cB1d074A26afFef007E98FB; - - /// @notice scaling factor for USDC - uint256 public constant USDC_SCALING_FACTOR = 1e12; - - function setUp() public { - cDai.accrueInterest(); - cUsdc.accrueInterest(); - - // deal small amounts of DAI/USDC to ensure deposits are not empty - // (onchain conditions might change and the tests reverts if one - // or both of the deposits is empty) - address holder = 0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7; // 3pool - vm.deal(holder, 1 ether); - // USDC - vm.prank(holder); - usdc.transfer(address(usdcDeposit), 1000e6); - usdcDeposit.deposit(); - // DAI - vm.prank(holder); - dai.transfer(address(daiDeposit), 1000e18); - daiDeposit.deposit(); - } - - function testSetup() public { - assertTrue(core.isPCVController(address(compoundRouter))); - - assertEq( - address(compoundRouter.daiPSM()), - MainnetAddresses.MAKER_DAI_USDC_PSM - ); - assertEq(address(compoundRouter.daiPcvDeposit()), address(daiDeposit)); - assertEq( - address(compoundRouter.usdcPcvDeposit()), - address(usdcDeposit) - ); - } - - function testSwapDaiToUsdcSucceeds() public { - uint256 withdrawAmount = daiDeposit.balance(); - uint256 usdcStartingBalance = usdcDeposit.balance(); - - vm.prank(governor); - compoundRouter.swapDaiForUsdc(withdrawAmount); /// withdraw all balance - - assertApproxEq( - usdcDeposit.balance().toInt256(), - (withdrawAmount / USDC_SCALING_FACTOR + usdcStartingBalance) - .toInt256(), - 0 - ); - assertTrue(daiDeposit.balance() < 1e10); /// assert only dust remains - } - - function testSwapUsdcToDaiSucceeds() public { - uint256 withdrawAmount = usdcDeposit.balance(); - uint256 daiStartingBalance = daiDeposit.balance(); - - vm.prank(governor); - compoundRouter.swapUsdcForDai(withdrawAmount); /// withdraw all balance - - assertApproxEq( - daiDeposit.balance().toInt256(), - (withdrawAmount * USDC_SCALING_FACTOR + daiStartingBalance) - .toInt256(), - 0 - ); - - assertTrue(usdcDeposit.balance() < 1e3); /// assert only dust remains - } - - function testSwapUsdcToDaiSucceedsPCVGuard() public { - uint256 withdrawAmount = usdcDeposit.balance(); - uint256 daiStartingBalance = daiDeposit.balance(); - - vm.prank(pcvGuard); - compoundRouter.swapUsdcForDai(withdrawAmount); /// withdraw all balance - - assertApproxEq( - daiDeposit.balance().toInt256(), - (withdrawAmount * USDC_SCALING_FACTOR + daiStartingBalance) - .toInt256(), - 0 - ); - assertTrue(usdcDeposit.balance() < 1e3); /// assert only dust remains - } - - function testSwapDaiToUsdcSucceedsPCVGuard() public { - uint256 withdrawAmount = daiDeposit.balance(); - uint256 usdcStartingBalance = usdcDeposit.balance(); - - vm.prank(pcvGuard); - compoundRouter.swapDaiForUsdc(withdrawAmount); /// withdraw all balance - - assertApproxEq( - usdcDeposit.balance().toInt256(), - (withdrawAmount / USDC_SCALING_FACTOR + usdcStartingBalance) - .toInt256(), - 0 - ); - assertTrue(daiDeposit.balance() < 1e10); /// assert only dust remains - } - - function testSwapUsdcToDaiFailsNoLiquidity() public { - uint256 usdcBalance = usdc.balanceOf( - MainnetAddresses.KRAKEN_USDC_WHALE - ); - vm.prank(MainnetAddresses.KRAKEN_USDC_WHALE); - usdc.transfer(address(usdcDeposit), usdcBalance); - usdcDeposit.deposit(); - - uint256 withdrawAmount = usdcDeposit.balance(); - vm.startPrank(pcvGuard); - pcvGuardian.withdrawAllToSafeAddress(address(usdcDeposit)); /// pull all funds from usdc pcv deposit - vm.expectRevert(stdError.arithmeticError); - compoundRouter.swapUsdcForDai(withdrawAmount); /// withdraw all balance - vm.stopPrank(); - } - - function testSwapDaiToUsdcFailsNoLiquidity() public { - uint256 withdrawAmount = daiDeposit.balance(); - vm.startPrank(pcvGuard); - pcvGuardian.withdrawAllToSafeAddress(address(daiDeposit)); /// pull all funds from dai pcv deposit - vm.expectRevert(stdError.arithmeticError); /// CDAI has new cToken implementation that fails with a revert - compoundRouter.swapDaiForUsdc(withdrawAmount); /// withdraw all balance - vm.stopPrank(); - } - - function testSwapUsdcToDaiFailsUnauthorized() public { - uint256 withdrawAmount = usdcDeposit.balance(); - vm.expectRevert("UNAUTHORIZED"); - compoundRouter.swapUsdcForDai(withdrawAmount); - } - - function testSwapDaiToUsdcFailsUnauthorized() public { - uint256 withdrawAmount = daiDeposit.balance(); - vm.expectRevert("UNAUTHORIZED"); - compoundRouter.swapDaiForUsdc(withdrawAmount); - } - - function testSwapUsdcToDaiFailsTinNonZero() public { - vm.prank(makerWard); - daiPSM.file(bytes32("tin"), 1); - - uint256 withdrawAmount = usdcDeposit.balance(); - - vm.prank(pcvGuard); - vm.expectRevert("CompoundPCVRouter: maker fee not 0"); - compoundRouter.swapUsdcForDai(withdrawAmount); - } - - function testSwapDaiToUsdcFailsToutNonZero() public { - vm.prank(makerWard); - daiPSM.file(bytes32("tout"), 1); - - uint256 withdrawAmount = daiDeposit.balance(); - - vm.prank(pcvGuard); - vm.expectRevert("CompoundPCVRouter: maker fee not 0"); - compoundRouter.swapDaiForUsdc(withdrawAmount); /// withdraw all balance - } -} diff --git a/contracts/test/integration/vip/vip14.sol b/contracts/test/integration/vip/vip14.sol deleted file mode 100644 index fad947ef0..000000000 --- a/contracts/test/integration/vip/vip14.sol +++ /dev/null @@ -1,300 +0,0 @@ -//SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity =0.8.13; - -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; -import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {TimelockController} from "@openzeppelin/contracts/governance/TimelockController.sol"; - -import {Vm} from "./../../unit/utils/Vm.sol"; -import {Core} from "../../../core/Core.sol"; -import {IVIP} from "./IVIP.sol"; -import {DSTest} from "./../../unit/utils/DSTest.sol"; -import {PCVDeposit} from "../../../pcv/PCVDeposit.sol"; -import {PCVGuardian} from "../../../pcv/PCVGuardian.sol"; -import {IPCVDeposit} from "../../../pcv/IPCVDeposit.sol"; -import {ERC20Allocator} from "../../../pcv/utils/ERC20Allocator.sol"; -import {MainnetAddresses} from "../fixtures/MainnetAddresses.sol"; -import {CompoundPCVRouter} from "../../../pcv/compound/CompoundPCVRouter.sol"; -import {ITimelockSimulation} from "../utils/ITimelockSimulation.sol"; -import {MorphoPCVDeposit} from "../../../pcv/morpho/MorphoPCVDeposit.sol"; - -/// Deployment Steps -/// 1. deploy morpho dai deposit -/// 2. deploy morpho usdc deposit -/// 3. deploy compound pcv router pointed to morpho dai and usdc deposits - -/// Governance Steps -/// 1. grant new PCV router PCV Controller role -/// 2. revoke PCV Controller role from old PCV Router - -/// 3. disconnect old dai compound deposit from allocator -/// 4. disconnect old usdc compound deposit from allocator - -/// 5. connect new dai morpho deposit to allocator -/// 6. connect new usdc morpho deposit to allocator - -/// 7. add morpho deposits as safe addresses - -/// 8. pause dai compound pcv deposit -/// 9. pause usdc compound pcv deposit - -contract vip14 is DSTest, IVIP { - using SafeCast for uint256; - using SafeERC20 for IERC20; - Vm public constant vm = Vm(HEVM_ADDRESS); - - address public immutable dai = MainnetAddresses.DAI; - address public immutable fei = MainnetAddresses.FEI; - address public immutable usdc = MainnetAddresses.USDC; - address public immutable core = MainnetAddresses.CORE; - - ITimelockSimulation.action[] private mainnetProposal; - ITimelockSimulation.action[] private arbitrumProposal; - - CompoundPCVRouter public router = - CompoundPCVRouter(MainnetAddresses.MORPHO_COMPOUND_PCV_ROUTER); - MorphoPCVDeposit public daiDeposit = - MorphoPCVDeposit(MainnetAddresses.MORPHO_COMPOUND_DAI_PCV_DEPOSIT); - MorphoPCVDeposit public usdcDeposit = - MorphoPCVDeposit(MainnetAddresses.MORPHO_COMPOUND_USDC_PCV_DEPOSIT); - - PCVGuardian public immutable pcvGuardian = - PCVGuardian(MainnetAddresses.PCV_GUARDIAN); - - ERC20Allocator public immutable allocator = - ERC20Allocator(MainnetAddresses.ERC20ALLOCATOR); - - constructor() { - if (block.chainid != 1) return; /// keep ci pipeline happy - - address[] memory toWhitelist = new address[](2); - toWhitelist[0] = address(daiDeposit); - toWhitelist[1] = address(usdcDeposit); - - mainnetProposal.push( - ITimelockSimulation.action({ - value: 0, - target: MainnetAddresses.CORE, - arguments: abi.encodeWithSignature( - "grantPCVController(address)", - address(router) - ), - description: "Grant Morpho PCV Router PCV Controller Role" - }) - ); - mainnetProposal.push( - ITimelockSimulation.action({ - value: 0, - target: MainnetAddresses.CORE, - arguments: abi.encodeWithSignature( - "revokePCVController(address)", - MainnetAddresses.COMPOUND_PCV_ROUTER - ), - description: "Revoke PCV Controller Role from Compound PCV Router" - }) - ); - - /// disconnect unused compound deposits - mainnetProposal.push( - ITimelockSimulation.action({ - value: 0, - target: MainnetAddresses.ERC20ALLOCATOR, - arguments: abi.encodeWithSignature( - "deleteDeposit(address)", - MainnetAddresses.COMPOUND_DAI_PCV_DEPOSIT - ), - description: "Remove Compound DAI Deposit from ERC20Allocator" - }) - ); - mainnetProposal.push( - ITimelockSimulation.action({ - value: 0, - target: MainnetAddresses.ERC20ALLOCATOR, - arguments: abi.encodeWithSignature( - "deleteDeposit(address)", - MainnetAddresses.COMPOUND_USDC_PCV_DEPOSIT - ), - description: "Remove Compound USDC Deposit from ERC20Allocator" - }) - ); - - /// connect to compound deposits - mainnetProposal.push( - ITimelockSimulation.action({ - value: 0, - target: MainnetAddresses.ERC20ALLOCATOR, - arguments: abi.encodeWithSignature( - "connectDeposit(address,address)", - MainnetAddresses.VOLT_DAI_PSM, - address(daiDeposit) - ), - description: "Add Morpho DAI Deposit to ERC20Allocator" - }) - ); - mainnetProposal.push( - ITimelockSimulation.action({ - value: 0, - target: MainnetAddresses.ERC20ALLOCATOR, - arguments: abi.encodeWithSignature( - "connectDeposit(address,address)", - MainnetAddresses.VOLT_USDC_PSM, - address(usdcDeposit) - ), - description: "Add Morpho USDC Deposit to ERC20Allocator" - }) - ); - - mainnetProposal.push( - ITimelockSimulation.action({ - value: 0, - target: MainnetAddresses.PCV_GUARDIAN, - arguments: abi.encodeWithSignature( - "addWhitelistAddresses(address[])", - toWhitelist - ), - description: "Add USDC and DAI Morpho deposits to the PCV Guardian" - }) - ); - - mainnetProposal.push( - ITimelockSimulation.action({ - value: 0, - target: MainnetAddresses.COMPOUND_DAI_PCV_DEPOSIT, - arguments: abi.encodeWithSignature("pause()"), - description: "Pause Compound DAI PCV Deposit" - }) - ); - - mainnetProposal.push( - ITimelockSimulation.action({ - value: 0, - target: MainnetAddresses.COMPOUND_USDC_PCV_DEPOSIT, - arguments: abi.encodeWithSignature("pause()"), - description: "Pause Compound USDC PCV Deposit" - }) - ); - } - - function getMainnetProposal() - public - view - override - returns (ITimelockSimulation.action[] memory) - { - return mainnetProposal; - } - - /// move all funds from compound deposits to morpho deposits - function mainnetSetup() public override { - vm.startPrank(MainnetAddresses.GOVERNOR); - pcvGuardian.withdrawAllToSafeAddress( - MainnetAddresses.COMPOUND_DAI_PCV_DEPOSIT - ); - pcvGuardian.withdrawAllToSafeAddress( - MainnetAddresses.COMPOUND_USDC_PCV_DEPOSIT - ); - - uint256 usdcBalance = IERC20(usdc).balanceOf(MainnetAddresses.GOVERNOR); - IERC20(usdc).transfer(address(usdcDeposit), usdcBalance); - IERC20(dai).transfer( - address(daiDeposit), - IERC20(dai).balanceOf(MainnetAddresses.GOVERNOR) - ); - - usdcDeposit.deposit(); - daiDeposit.deposit(); - - vm.stopPrank(); - } - - /// assert core addresses are set correctly - /// assert dai and usdc compound pcv deposit are pcv guardian in whitelist - /// assert pcv deposits are set correctly in router - /// assert pcv deposits are set correctly in allocator - /// assert old pcv deposits are disconnected in allocator - function mainnetValidate() public override { - assertEq(address(usdcDeposit.core()), core); - assertEq(address(daiDeposit.core()), core); - assertEq(address(router.core()), core); - - assertEq(daiDeposit.morpho(), MainnetAddresses.MORPHO); - assertEq(usdcDeposit.morpho(), MainnetAddresses.MORPHO); - assertEq(daiDeposit.lens(), MainnetAddresses.MORPHO_LENS); - assertEq(usdcDeposit.lens(), MainnetAddresses.MORPHO_LENS); - - assertTrue(Core(core).isPCVController(address(router))); - assertTrue( - !Core(core).isPCVController(MainnetAddresses.COMPOUND_PCV_ROUTER) - ); - - /// pcv guardian whitelist assertions - assertTrue(pcvGuardian.isWhitelistAddress(address(daiDeposit))); - assertTrue(pcvGuardian.isWhitelistAddress(address(usdcDeposit))); - - /// router parameter validations - assertEq(address(router.daiPcvDeposit()), address(daiDeposit)); - assertEq(address(router.usdcPcvDeposit()), address(usdcDeposit)); - assertEq(address(router.DAI()), dai); - assertEq(address(router.USDC()), usdc); - assertEq(address(router.GEM_JOIN()), MainnetAddresses.GEM_JOIN); - assertEq(address(router.daiPSM()), MainnetAddresses.MAKER_DAI_USDC_PSM); - - /// old deposits paused - assertTrue( - PCVDeposit(MainnetAddresses.COMPOUND_USDC_PCV_DEPOSIT).paused() - ); - assertTrue( - PCVDeposit(MainnetAddresses.COMPOUND_DAI_PCV_DEPOSIT).paused() - ); - - /// old deposits disconnected in allocator - assertEq( - allocator.pcvDepositToPSM( - MainnetAddresses.COMPOUND_USDC_PCV_DEPOSIT - ), - address(0) - ); - assertEq( - allocator.pcvDepositToPSM( - MainnetAddresses.COMPOUND_DAI_PCV_DEPOSIT - ), - address(0) - ); - - /// new deposits connected in allocator - assertEq( - allocator.pcvDepositToPSM(address(daiDeposit)), - MainnetAddresses.VOLT_DAI_PSM - ); - assertEq( - allocator.pcvDepositToPSM(address(usdcDeposit)), - MainnetAddresses.VOLT_USDC_PSM - ); - - /// new pcv deposits set up correctly - assertEq(usdcDeposit.token(), MainnetAddresses.USDC); - assertEq(daiDeposit.token(), MainnetAddresses.DAI); - - assertEq(usdcDeposit.cToken(), MainnetAddresses.CUSDC); - assertEq(daiDeposit.cToken(), MainnetAddresses.CDAI); - } - - function getArbitrumProposal() - public - pure - override - returns (ITimelockSimulation.action[] memory) - { - revert("no arbitrum proposal"); - } - - /// no-op, nothing to setup - function arbitrumSetup() public pure override { - revert("no arbitrum proposal"); - } - - function arbitrumValidate() public pure override { - revert("no arbitrum proposal"); - } -} From bd6faf7fd878666be490d176b32f2c27e7173da3 Mon Sep 17 00:00:00 2001 From: Elliot Date: Thu, 19 Jan 2023 20:53:16 -0800 Subject: [PATCH 16/74] Euler PCV Deposit --- src/pcv/euler/EulerPCVDeposit.sol | 77 +++++++++++++++++++++++++++++++ src/pcv/euler/IEToken.sol | 21 +++++++++ 2 files changed, 98 insertions(+) create mode 100644 src/pcv/euler/EulerPCVDeposit.sol create mode 100644 src/pcv/euler/IEToken.sol diff --git a/src/pcv/euler/EulerPCVDeposit.sol b/src/pcv/euler/EulerPCVDeposit.sol new file mode 100644 index 000000000..96abe9c91 --- /dev/null +++ b/src/pcv/euler/EulerPCVDeposit.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity =0.8.13; + +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {IEToken} from "@voltprotocol/pcv/euler/IEToken.sol"; +import {CoreRefV2} from "@voltprotocol/refs/CoreRefV2.sol"; +import {PCVDepositV2} from "@voltprotocol/pcv/PCVDepositV2.sol"; + +/// @notice PCV Deposit for Euler +contract EulerPCVDeposit is PCVDepositV2 { + using SafeERC20 for IERC20; + + /// @notice euler main contract that receives tokens + address public immutable eulerMain; + + /// @notice reference to the euler token that represents an asset + IEToken public immutable eToken; + + /// @notice sub-account id for euler + uint256 public constant subAccountId = 0; + + /// @notice fetch underlying asset by calling pool and getting liquidity asset + /// @param _core reference to the Core contract + /// @param _eToken reference to the euler asset token + /// @param _eulerMain reference to the euler main address + constructor( + address _core, + address _eToken, + address _eulerMain, + address _underlying, + address _rewardToken + ) PCVDepositV2(_underlying, _rewardToken) CoreRefV2(_core) { + eToken = IEToken(_eToken); + eulerMain = _eulerMain; + address underlying = IEToken(_eToken).underlyingAsset(); + + require(underlying == _underlying, "EulerDeposit: underlying mismatch"); + } + + /// @notice return the amount of funds this contract owns in underlying tokens + function balance() public view override returns (uint256) { + return eToken.balanceOfUnderlying(address(this)); + } + + /// @notice deposit PCV into Euler. + function _supply(uint256 amount) internal override { + /// approve euler main to spend underlying token + IERC20(token).approve(eulerMain, amount); + /// deposit into eToken + eToken.deposit(subAccountId, amount); + } + + /// @notice withdraw PCV from Euler, only callable by PCV controller + /// @param to destination after funds are withdrawn from venue + /// @param amount of PCV to withdraw from the venue + function _withdrawAndTransfer( + uint256 amount, + address to + ) internal override { + eToken.withdraw(subAccountId, amount); + IERC20(token).safeTransfer(to, amount); + } + + /// @notice this is a no-op + /// euler distributes tokens through a merkle drop, + /// no need for claim functionality + function _claim() internal pure override returns (uint256) { + return 0; + } + + /// @notice this is a no-op + /// euler automatically gets the most up to date balance of each user + /// and does not require any poking + function _accrueUnderlying() internal override {} +} diff --git a/src/pcv/euler/IEToken.sol b/src/pcv/euler/IEToken.sol new file mode 100644 index 000000000..236a79140 --- /dev/null +++ b/src/pcv/euler/IEToken.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity =0.8.13; + +interface IEToken { + /// @notice withdraw from euler + function withdraw(uint256 subAccountId, uint256 amount) external; + + /// @notice deposit into euler + function deposit(uint256 subAccountId, uint256 amount) external; + + /// @notice returns balance of underlying including all interest accrued + function balanceOfUnderlying( + address account + ) external view returns (uint256); + + /// @notice returns address of underlying token + function underlyingAsset() external view returns (address); + + /// @notice returns balance of address + function balanceOf(address) external view returns (uint256); +} From 0561407cf6a3f93d38a480777c17f25e05dbbb25 Mon Sep 17 00:00:00 2001 From: Elliot Date: Thu, 19 Jan 2023 20:54:33 -0800 Subject: [PATCH 17/74] Morpho AAVE PCV Deposit --- src/pcv/morpho/MorphoAavePCVDeposit.sol | 125 ++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 src/pcv/morpho/MorphoAavePCVDeposit.sol diff --git a/src/pcv/morpho/MorphoAavePCVDeposit.sol b/src/pcv/morpho/MorphoAavePCVDeposit.sol new file mode 100644 index 000000000..948e38b18 --- /dev/null +++ b/src/pcv/morpho/MorphoAavePCVDeposit.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity =0.8.13; + +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {ILens} from "@voltprotocol/pcv/morpho/ILens.sol"; +import {IMorpho} from "@voltprotocol/pcv/morpho/IMorpho.sol"; +import {CoreRefV2} from "@voltprotocol/refs/CoreRefV2.sol"; +import {Constants} from "@voltprotocol/Constants.sol"; +import {PCVDepositV2} from "@voltprotocol/pcv/PCVDepositV2.sol"; + +/// @notice PCV Deposit for Morpho-Aave V2. +/// Implements the PCV Deposit interface to deposit and withdraw funds in Morpho +/// Liquidity profile of Morpho for this deposit is fully liquid for USDC and DAI +/// because the incentivized rates are higher than the P2P rate. +/// Only for depositing USDC and DAI. USDT is not in scope. +/// @dev approves the Morpho Deposit to spend this PCV deposit's token, +/// and then calls supply on Morpho, which pulls the underlying token to Morpho, +/// drawing down on the approved amount to be spent, +/// and then giving this PCV Deposit credits on Morpho in exchange for the underlying +/// @dev PCV Guardian functions withdrawERC20ToSafeAddress and withdrawAllERC20ToSafeAddress +/// will not work with removing Morpho Tokens on the Morpho PCV Deposit because Morpho +/// has no concept of mTokens. This means if the contract is paused, or an issue is +/// surfaced in Morpho and liquidity is locked, Volt will need to rely on social +/// coordination with the Morpho team to recover funds. +/// @dev Depositing and withdrawing in a single block will cause a very small loss +/// of funds, less than a pip. The way to not realize this loss is by depositing and +/// then withdrawing at least 1 block later. That way, interest accrues. +/// This is not a Morpho specific issue. +/// TODO confirm that Aave rounds in the protocol's favor. +/// The issue is caused by constraints inherent to solidity and the EVM. +/// There are no floating point numbers, this means there is precision loss, +/// and protocol engineers are forced to choose who to round in favor of. +/// Engineers must round in favor of the protocol to avoid deposits of 0 giving +/// the user a balance. +contract MorphoAavePCVDeposit is PCVDepositV2 { + using SafeERC20 for IERC20; + + /// ------------------------------------------ + /// ---------- Immutables/Constant ----------- + /// ------------------------------------------ + + /// @notice reference to the lens contract for morpho-aave v2 + address public immutable lens; + + /// @notice reference to the morpho-aave v2 market + address public immutable morpho; + + /// @notice aToken in aave this deposit tracks + /// used to inform morpho about the desired market to supply liquidity + address public immutable aToken; + + /// @param _core reference to the core contract + /// @param _aToken aToken this deposit references + /// @param _underlying Token denomination of this deposit + /// @param _rewardToken Reward token denomination of this deposit + /// @param _morpho reference to the morpho-compound v2 market + /// @param _lens reference to the morpho-compound v2 lens + constructor( + address _core, + address _aToken, + address _underlying, + address _rewardToken, + address _morpho, + address _lens + ) PCVDepositV2(_underlying, _rewardToken) CoreRefV2(_core) { + aToken = _aToken; + morpho = _morpho; + lens = _lens; + } + + /// ------------------------------------------ + /// ------------------ Views ----------------- + /// ------------------------------------------ + + /// @notice Returns the distribution of assets supplied by this contract through Morpho-Compound. + /// @return sum of suppliedP2P and suppliedOnPool for the given aToken + function balance() public view override returns (uint256) { + (, , uint256 totalSupplied) = ILens(lens).getCurrentSupplyBalanceInOf( + aToken, + address(this) + ); + + return totalSupplied; + } + + /// ------------------------------------------ + /// ------------- Helper Methods ------------- + /// ------------------------------------------ + + /// @notice accrue interest in the underlying morpho venue + function _accrueUnderlying() internal override { + /// accrue interest in Morpho Aave + IMorpho(morpho).updateIndexes(aToken); + } + + /// @dev withdraw from the underlying morpho market. + function _withdrawAndTransfer( + uint256 amount, + address to + ) internal override { + IMorpho(morpho).withdraw(aToken, amount); + IERC20(token).safeTransfer(to, amount); + } + + /// @dev deposit in the underlying morpho market. + function _supply(uint256 amount) internal override { + IERC20(token).approve(address(morpho), amount); + IMorpho(morpho).supply( + aToken, /// aToken to supply liquidity to + address(this), /// the address of the user you want to supply on behalf of + amount + ); + } + + /// @dev claim rewards from the underlying aave market. + /// returns amount of reward tokens claimed + function _claim() internal override returns (uint256) { + address[] memory aTokens = new address[](1); + aTokens[0] = aToken; + + return IMorpho(morpho).claimRewards(aTokens, false); /// bool set false to receive COMP + } +} From 7c707214ffc75f412a9bd2f1aeaef1001f13e8d9 Mon Sep 17 00:00:00 2001 From: Elliot Date: Thu, 19 Jan 2023 20:55:12 -0800 Subject: [PATCH 18/74] move balanceReportedIn to PCV Deposit V2 --- src/pcv/PCVDepositV2.sol | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/pcv/PCVDepositV2.sol b/src/pcv/PCVDepositV2.sol index f13d5b76f..72892e14d 100644 --- a/src/pcv/PCVDepositV2.sol +++ b/src/pcv/PCVDepositV2.sol @@ -45,6 +45,15 @@ abstract contract PCVDepositV2 is IPCVDepositV2, CoreRefV2 { rewardToken = _rewardToken; } + /// ------------------------------------------ + /// -------------- View Only API ------------- + /// ------------------------------------------ + + /// @notice return the underlying token denomination for this deposit + function balanceReportedIn() external view returns (address) { + return address(token); + } + /// ------------------------------------------ /// ----------- Permissionless API ----------- /// ------------------------------------------ From 2fc0868c2dc6ea19416ffaac5a05e8879df06328 Mon Sep 17 00:00:00 2001 From: Elliot Date: Thu, 19 Jan 2023 20:55:35 -0800 Subject: [PATCH 19/74] Morpho Aave and Euler PCV Deposit Tests --- ...egrationTestMorphoCompoundPCVDeposit.t.sol | 2 +- ...IntegrationTestCompoundBadDebtSentinel.sol | 10 +- .../IntegrationTestEulerPCVDeposit.sol | 129 +++++++++++++++++ .../IntegrationTestMorphoAavePCVDeposit.sol | 133 ++++++++++++++++++ .../IntegrationTestPCVGuardian.sol | 15 +- .../IntegrationTestPCVOracle.sol | 4 +- .../IntegrationTestPCVRouter.sol | 4 +- .../IntegrationTestRateLimiters.sol | 8 +- .../IntegrationTestRoles.sol | 44 +++++- 9 files changed, 328 insertions(+), 21 deletions(-) create mode 100644 test/integration/post-proposal-checks/IntegrationTestEulerPCVDeposit.sol create mode 100644 test/integration/post-proposal-checks/IntegrationTestMorphoAavePCVDeposit.sol diff --git a/test/integration/IntegrationTestMorphoCompoundPCVDeposit.t.sol b/test/integration/IntegrationTestMorphoCompoundPCVDeposit.t.sol index 34cb4b449..a45681253 100644 --- a/test/integration/IntegrationTestMorphoCompoundPCVDeposit.t.sol +++ b/test/integration/IntegrationTestMorphoCompoundPCVDeposit.t.sol @@ -90,7 +90,7 @@ contract IntegrationTestMorphoPCVDeposit is Test { vm.label(address(usdc), "USDC"); vm.label(address(dai), "DAI"); vm.label(0x930f1b46e1D081Ec1524efD95752bE3eCe51EF67, "Morpho Lens"); - vm.label(0x8888882f8f843896699869179fB6E4f7e3B58888, "Morpho"); + vm.label(0x8888882f8f843896699869179fB6E4f7e3B58888, "MORPHO_COMPOUND"); vm.startPrank(DAI_USDC_USDT_CURVE_POOL); dai.transfer(address(daiDeposit), targetDaiBalance); diff --git a/test/integration/post-proposal-checks/IntegrationTestCompoundBadDebtSentinel.sol b/test/integration/post-proposal-checks/IntegrationTestCompoundBadDebtSentinel.sol index bcc2fe496..62f895bd8 100644 --- a/test/integration/post-proposal-checks/IntegrationTestCompoundBadDebtSentinel.sol +++ b/test/integration/post-proposal-checks/IntegrationTestCompoundBadDebtSentinel.sol @@ -17,10 +17,10 @@ contract IntegrationTestCompoundBadDebtSentinel is PostProposalCheck { addresses.mainnet("PCV_GUARDIAN") ); IPCVDepositV2 daiDeposit = IPCVDepositV2( - addresses.mainnet("PCV_DEPOSIT_MORPHO_DAI") + addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_DAI") ); IPCVDepositV2 usdcDeposit = IPCVDepositV2( - addresses.mainnet("PCV_DEPOSIT_MORPHO_USDC") + addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_USDC") ); address yearn = 0x342491C093A640c7c2347c4FFA7D8b9cBC84D1EB; @@ -58,17 +58,17 @@ contract IntegrationTestCompoundBadDebtSentinel is PostProposalCheck { addresses.mainnet("COMPOUND_BAD_DEBT_SENTINEL") ); IPCVDepositV2 daiDeposit = IPCVDepositV2( - addresses.mainnet("PCV_DEPOSIT_MORPHO_DAI") + addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_DAI") ); IPCVDepositV2 usdcDeposit = IPCVDepositV2( - addresses.mainnet("PCV_DEPOSIT_MORPHO_USDC") + addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_USDC") ); address yearn = 0x342491C093A640c7c2347c4FFA7D8b9cBC84D1EB; address[] memory users = new address[](2); users[0] = yearn; /// yearn is less than morpho, place it first to order list - users[1] = addresses.mainnet("MORPHO"); + users[1] = addresses.mainnet("MORPHO_COMPOUND"); assertEq(badDebtSentinel.getTotalBadDebt(users), 0); diff --git a/test/integration/post-proposal-checks/IntegrationTestEulerPCVDeposit.sol b/test/integration/post-proposal-checks/IntegrationTestEulerPCVDeposit.sol new file mode 100644 index 000000000..8ff97ccf2 --- /dev/null +++ b/test/integration/post-proposal-checks/IntegrationTestEulerPCVDeposit.sol @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.13; + +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {console} from "@forge-std/console.sol"; +import {PCVGuardian} from "@voltprotocol/pcv/PCVGuardian.sol"; +import {SystemEntry} from "@voltprotocol/entry/SystemEntry.sol"; +import {IPCVDepositV2} from "@voltprotocol/pcv/IPCVDepositV2.sol"; +import {EulerPCVDeposit} from "@voltprotocol/pcv/euler/EulerPCVDeposit.sol"; +import {PostProposalCheck} from "@test/integration/post-proposal-checks/PostProposalCheck.sol"; + +contract IntegrationTestEulerPCVDeposit is PostProposalCheck { + using SafeCast for *; + + uint256 depositAmount = 1_000_000; + + function testCanDepositEuler() public { + SystemEntry entry = SystemEntry(addresses.mainnet("SYSTEM_ENTRY")); + EulerPCVDeposit daiDeposit = EulerPCVDeposit( + addresses.mainnet("PCV_DEPOSIT_EULER_DAI") + ); + EulerPCVDeposit usdcDeposit = EulerPCVDeposit( + addresses.mainnet("PCV_DEPOSIT_EULER_USDC") + ); + + deal( + addresses.mainnet("DAI"), + address(daiDeposit), + depositAmount * 1e18 + ); + deal( + addresses.mainnet("USDC"), + address(usdcDeposit), + depositAmount * 1e6 + ); + + entry.deposit(address(daiDeposit)); + entry.deposit(address(usdcDeposit)); + + assertApproxEq( + daiDeposit.balance().toInt256(), + (depositAmount * 1e18).toInt256(), + 0 + ); + assertApproxEq( + usdcDeposit.balance().toInt256(), + (depositAmount * 1e6).toInt256(), + 0 + ); + } + + function testHarvestEuler() public { + testCanDepositEuler(); + + SystemEntry entry = SystemEntry(addresses.mainnet("SYSTEM_ENTRY")); + EulerPCVDeposit daiDeposit = EulerPCVDeposit( + addresses.mainnet("PCV_DEPOSIT_EULER_DAI") + ); + EulerPCVDeposit usdcDeposit = EulerPCVDeposit( + addresses.mainnet("PCV_DEPOSIT_EULER_USDC") + ); + + entry.harvest(address(daiDeposit)); + entry.harvest(address(usdcDeposit)); + } + + function testAccrueEuler() public { + testCanDepositEuler(); + + SystemEntry entry = SystemEntry(addresses.mainnet("SYSTEM_ENTRY")); + EulerPCVDeposit daiDeposit = EulerPCVDeposit( + addresses.mainnet("PCV_DEPOSIT_EULER_DAI") + ); + EulerPCVDeposit usdcDeposit = EulerPCVDeposit( + addresses.mainnet("PCV_DEPOSIT_EULER_USDC") + ); + + uint256 daiBalance = entry.accrue(address(daiDeposit)); + uint256 usdcBalance = entry.accrue(address(usdcDeposit)); + + assertApproxEq( + daiBalance.toInt256(), + (depositAmount * 1e18).toInt256(), + 0 + ); + assertApproxEq( + usdcBalance.toInt256(), + (depositAmount * 1e6).toInt256(), + 0 + ); + } + + function testWithdrawPCVGuardian() public { + testCanDepositEuler(); + + PCVGuardian pcvGuardian = PCVGuardian( + addresses.mainnet("PCV_GUARDIAN") + ); + + address[2] memory addressesToClean = [ + addresses.mainnet("PCV_DEPOSIT_EULER_DAI"), + addresses.mainnet("PCV_DEPOSIT_EULER_USDC") + ]; + + vm.startPrank(addresses.mainnet("GOVERNOR")); + for (uint256 i = 0; i < addressesToClean.length; i++) { + pcvGuardian.withdrawAllToSafeAddress(addressesToClean[i]); + // Check only dust left after withdrawals + assertLt(IPCVDepositV2(addressesToClean[i]).balance(), 1e6); + } + vm.stopPrank(); + + // sanity checks + address safeAddress = pcvGuardian.safeAddress(); + require(safeAddress != address(0), "Safe address is 0 address"); + assertApproxEq( + IERC20(addresses.mainnet("DAI")).balanceOf(safeAddress).toInt256(), + (depositAmount * 1e18).toInt256(), + 0 + ); // ~1M DAI + assertApproxEq( + IERC20(addresses.mainnet("USDC")).balanceOf(safeAddress).toInt256(), + (depositAmount * 1e6).toInt256(), + 0 + ); // ~1M USDC + } +} diff --git a/test/integration/post-proposal-checks/IntegrationTestMorphoAavePCVDeposit.sol b/test/integration/post-proposal-checks/IntegrationTestMorphoAavePCVDeposit.sol new file mode 100644 index 000000000..74fd8faef --- /dev/null +++ b/test/integration/post-proposal-checks/IntegrationTestMorphoAavePCVDeposit.sol @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.13; + +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {console} from "@forge-std/console.sol"; +import {PCVGuardian} from "@voltprotocol/pcv/PCVGuardian.sol"; +import {SystemEntry} from "@voltprotocol/entry/SystemEntry.sol"; +import {IPCVDepositV2} from "@voltprotocol/pcv/IPCVDepositV2.sol"; +import {PostProposalCheck} from "@test/integration/post-proposal-checks/PostProposalCheck.sol"; +import {MorphoAavePCVDeposit} from "@voltprotocol/pcv/morpho/MorphoAavePCVDeposit.sol"; + +contract IntegrationTestMorphoAavePCVDeposit is PostProposalCheck { + using SafeCast for *; + + uint256 depositAmount = 1_000_000; + + function testCanDepositAave() public { + SystemEntry entry = SystemEntry(addresses.mainnet("SYSTEM_ENTRY")); + MorphoAavePCVDeposit daiDeposit = MorphoAavePCVDeposit( + addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_DAI") + ); + MorphoAavePCVDeposit usdcDeposit = MorphoAavePCVDeposit( + addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_USDC") + ); + + deal( + addresses.mainnet("DAI"), + address(daiDeposit), + depositAmount * 1e18 + ); + deal( + addresses.mainnet("USDC"), + address(usdcDeposit), + depositAmount * 1e6 + ); + + entry.deposit(address(daiDeposit)); + entry.deposit(address(usdcDeposit)); + + assertApproxEq( + daiDeposit.balance().toInt256(), + (depositAmount * 1e18).toInt256(), + 0 + ); + assertApproxEq( + usdcDeposit.balance().toInt256(), + (depositAmount * 1e6).toInt256(), + 0 + ); + } + + /// liquidity mining is over for aave, so harvesting fails + function testHarvestFailsAave() public { + testCanDepositAave(); + + SystemEntry entry = SystemEntry(addresses.mainnet("SYSTEM_ENTRY")); + MorphoAavePCVDeposit daiDeposit = MorphoAavePCVDeposit( + addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_DAI") + ); + MorphoAavePCVDeposit usdcDeposit = MorphoAavePCVDeposit( + addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_USDC") + ); + + vm.expectRevert(); + entry.harvest(address(daiDeposit)); + + vm.expectRevert(); + entry.harvest(address(usdcDeposit)); + } + + function testAccrueAave() public { + testCanDepositAave(); + + SystemEntry entry = SystemEntry(addresses.mainnet("SYSTEM_ENTRY")); + MorphoAavePCVDeposit daiDeposit = MorphoAavePCVDeposit( + addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_DAI") + ); + MorphoAavePCVDeposit usdcDeposit = MorphoAavePCVDeposit( + addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_USDC") + ); + + uint256 daiBalance = entry.accrue(address(daiDeposit)); + uint256 usdcBalance = entry.accrue(address(usdcDeposit)); + + assertApproxEq( + daiBalance.toInt256(), + (depositAmount * 1e18).toInt256(), + 0 + ); + assertApproxEq( + usdcBalance.toInt256(), + (depositAmount * 1e6).toInt256(), + 0 + ); + } + + function testWithdrawPCVGuardian() public { + testCanDepositAave(); + + PCVGuardian pcvGuardian = PCVGuardian( + addresses.mainnet("PCV_GUARDIAN") + ); + + address[2] memory addressesToClean = [ + addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_DAI"), + addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_USDC") + ]; + + vm.startPrank(addresses.mainnet("GOVERNOR")); + for (uint256 i = 0; i < addressesToClean.length; i++) { + pcvGuardian.withdrawAllToSafeAddress(addressesToClean[i]); + // Check only dust left after withdrawals + assertLt(IPCVDepositV2(addressesToClean[i]).balance(), 1e6); + } + vm.stopPrank(); + + // sanity checks + address safeAddress = pcvGuardian.safeAddress(); + require(safeAddress != address(0), "Safe address is 0 address"); + assertApproxEq( + IERC20(addresses.mainnet("DAI")).balanceOf(safeAddress).toInt256(), + (depositAmount * 1e18).toInt256(), + 0 + ); // ~1M DAI + assertApproxEq( + IERC20(addresses.mainnet("USDC")).balanceOf(safeAddress).toInt256(), + (depositAmount * 1e6).toInt256(), + 0 + ); // ~1M USDC + } +} diff --git a/test/integration/post-proposal-checks/IntegrationTestPCVGuardian.sol b/test/integration/post-proposal-checks/IntegrationTestPCVGuardian.sol index 151d445cf..7a070392f 100644 --- a/test/integration/post-proposal-checks/IntegrationTestPCVGuardian.sol +++ b/test/integration/post-proposal-checks/IntegrationTestPCVGuardian.sol @@ -14,14 +14,21 @@ contract IntegrationTestPCVGuardian is PostProposalCheck { ); vm.startPrank(addresses.mainnet("GOVERNOR")); - address[4] memory addressesToClean = [ + address[8] memory addressesToClean = [ addresses.mainnet("PSM_DAI"), addresses.mainnet("PSM_USDC"), - addresses.mainnet("PCV_DEPOSIT_MORPHO_DAI"), - addresses.mainnet("PCV_DEPOSIT_MORPHO_USDC") + addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_DAI"), + addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_USDC"), + addresses.mainnet("PCV_DEPOSIT_EULER_DAI"), + addresses.mainnet("PCV_DEPOSIT_EULER_USDC"), + addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_DAI"), + addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_DAI") ]; for (uint256 i = 0; i < addressesToClean.length; i++) { - pcvGuardian.withdrawAllToSafeAddress(addressesToClean[i]); + if (IPCVDepositV2(addressesToClean[i]).balance() != 0) { + pcvGuardian.withdrawAllToSafeAddress(addressesToClean[i]); + } + // Check only dust left after withdrawals assertLt(IPCVDepositV2(addressesToClean[i]).balance(), 1e6); } diff --git a/test/integration/post-proposal-checks/IntegrationTestPCVOracle.sol b/test/integration/post-proposal-checks/IntegrationTestPCVOracle.sol index 95848354e..dfc904833 100644 --- a/test/integration/post-proposal-checks/IntegrationTestPCVOracle.sol +++ b/test/integration/post-proposal-checks/IntegrationTestPCVOracle.sol @@ -29,7 +29,9 @@ contract IntegrationTestPCVOracle is PostProposalCheck { dai = IERC20(addresses.mainnet("DAI")); volt = VoltV2(addresses.mainnet("VOLT")); grlm = addresses.mainnet("GLOBAL_RATE_LIMITED_MINTER"); - morphoDaiPCVDeposit = addresses.mainnet("PCV_DEPOSIT_MORPHO_DAI"); + morphoDaiPCVDeposit = addresses.mainnet( + "PCV_DEPOSIT_MORPHO_COMPOUND_DAI" + ); daipsm = PegStabilityModule(addresses.mainnet("PSM_DAI")); pcvOracle = PCVOracle(addresses.mainnet("PCV_ORACLE")); pcvGuardian = PCVGuardian(addresses.mainnet("PCV_GUARDIAN")); diff --git a/test/integration/post-proposal-checks/IntegrationTestPCVRouter.sol b/test/integration/post-proposal-checks/IntegrationTestPCVRouter.sol index 4cefd02eb..43acc9fda 100644 --- a/test/integration/post-proposal-checks/IntegrationTestPCVRouter.sol +++ b/test/integration/post-proposal-checks/IntegrationTestPCVRouter.sol @@ -15,10 +15,10 @@ contract IntegrationTestPCVRouter is PostProposalCheck { function testPcvRouterWithSwap() public { PCVRouter pcvRouter = PCVRouter(addresses.mainnet("PCV_ROUTER")); IPCVDepositV2 daiDeposit = IPCVDepositV2( - addresses.mainnet("PCV_DEPOSIT_MORPHO_DAI") + addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_DAI") ); IPCVDepositV2 usdcDeposit = IPCVDepositV2( - addresses.mainnet("PCV_DEPOSIT_MORPHO_USDC") + addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_USDC") ); address pcvMover = addresses.mainnet("GOVERNOR"); // an address with PCV_MOVER role diff --git a/test/integration/post-proposal-checks/IntegrationTestRateLimiters.sol b/test/integration/post-proposal-checks/IntegrationTestRateLimiters.sol index f554eae99..3b7937891 100644 --- a/test/integration/post-proposal-checks/IntegrationTestRateLimiters.sol +++ b/test/integration/post-proposal-checks/IntegrationTestRateLimiters.sol @@ -60,8 +60,12 @@ contract IntegrationTestRateLimiters is PostProposalCheck { addresses.mainnet("GLOBAL_SYSTEM_EXIT_RATE_LIMITER") ); pcvOracle = PCVOracle(addresses.mainnet("PCV_ORACLE")); - morphoUsdcPCVDeposit = addresses.mainnet("PCV_DEPOSIT_MORPHO_USDC"); - morphoDaiPCVDeposit = addresses.mainnet("PCV_DEPOSIT_MORPHO_DAI"); + morphoUsdcPCVDeposit = addresses.mainnet( + "PCV_DEPOSIT_MORPHO_COMPOUND_USDC" + ); + morphoDaiPCVDeposit = addresses.mainnet( + "PCV_DEPOSIT_MORPHO_COMPOUND_DAI" + ); } /* diff --git a/test/integration/post-proposal-checks/IntegrationTestRoles.sol b/test/integration/post-proposal-checks/IntegrationTestRoles.sol index 14db32256..e6b38b3d5 100644 --- a/test/integration/post-proposal-checks/IntegrationTestRoles.sol +++ b/test/integration/post-proposal-checks/IntegrationTestRoles.sol @@ -67,14 +67,30 @@ contract IntegrationTestRoles is PostProposalCheck { // PCV_DEPOSIT_ROLE assertEq(core.getRoleAdmin(VoltRoles.PCV_DEPOSIT), VoltRoles.GOVERNOR); - assertEq(core.getRoleMemberCount(VoltRoles.PCV_DEPOSIT), 2); + assertEq(core.getRoleMemberCount(VoltRoles.PCV_DEPOSIT), 6); assertEq( core.getRoleMember(VoltRoles.PCV_DEPOSIT, 0), - addresses.mainnet("PCV_DEPOSIT_MORPHO_DAI") + addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_DAI") ); assertEq( core.getRoleMember(VoltRoles.PCV_DEPOSIT, 1), - addresses.mainnet("PCV_DEPOSIT_MORPHO_USDC") + addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_USDC") + ); + assertEq( + core.getRoleMember(VoltRoles.PCV_DEPOSIT, 2), + addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_DAI") + ); + assertEq( + core.getRoleMember(VoltRoles.PCV_DEPOSIT, 3), + addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_USDC") + ); + assertEq( + core.getRoleMember(VoltRoles.PCV_DEPOSIT, 4), + addresses.mainnet("PCV_DEPOSIT_EULER_DAI") + ); + assertEq( + core.getRoleMember(VoltRoles.PCV_DEPOSIT, 5), + addresses.mainnet("PCV_DEPOSIT_EULER_USDC") ); // PCV_GUARD @@ -157,7 +173,7 @@ contract IntegrationTestRoles is PostProposalCheck { // LOCKER_ROLE assertEq(core.getRoleAdmin(VoltRoles.LOCKER), VoltRoles.GOVERNOR); - assertEq(core.getRoleMemberCount(VoltRoles.LOCKER), 13); + assertEq(core.getRoleMemberCount(VoltRoles.LOCKER), 17); assertEq( core.getRoleMember(VoltRoles.LOCKER, 0), addresses.mainnet("SYSTEM_ENTRY") @@ -180,11 +196,11 @@ contract IntegrationTestRoles is PostProposalCheck { ); assertEq( core.getRoleMember(VoltRoles.LOCKER, 5), - addresses.mainnet("PCV_DEPOSIT_MORPHO_DAI") + addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_DAI") ); assertEq( core.getRoleMember(VoltRoles.LOCKER, 6), - addresses.mainnet("PCV_DEPOSIT_MORPHO_USDC") + addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_USDC") ); assertEq( core.getRoleMember(VoltRoles.LOCKER, 7), @@ -210,6 +226,22 @@ contract IntegrationTestRoles is PostProposalCheck { core.getRoleMember(VoltRoles.LOCKER, 12), addresses.mainnet("PSM_NONCUSTODIAL_USDC") ); + assertEq( + core.getRoleMember(VoltRoles.LOCKER, 13), + addresses.mainnet("PCV_DEPOSIT_EULER_DAI") + ); + assertEq( + core.getRoleMember(VoltRoles.LOCKER, 14), + addresses.mainnet("PCV_DEPOSIT_EULER_USDC") + ); + assertEq( + core.getRoleMember(VoltRoles.LOCKER, 15), + addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_DAI") + ); + assertEq( + core.getRoleMember(VoltRoles.LOCKER, 16), + addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_USDC") + ); // MINTER assertEq(core.getRoleAdmin(VoltRoles.MINTER), VoltRoles.GOVERNOR); From dab899f51d441988807f176e4c8d2c5e1deb407e Mon Sep 17 00:00:00 2001 From: Elliot Date: Thu, 19 Jan 2023 20:56:22 -0800 Subject: [PATCH 20/74] Add AAVE Morpho updateIndex function --- src/pcv/morpho/IMorpho.sol | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/pcv/morpho/IMorpho.sol b/src/pcv/morpho/IMorpho.sol index 417e448f9..f3c64102a 100644 --- a/src/pcv/morpho/IMorpho.sol +++ b/src/pcv/morpho/IMorpho.sol @@ -21,5 +21,9 @@ interface IMorpho { function liquidate(address _poolTokenBorrowedAddress, address _poolTokenCollateralAddress, address _borrower, uint256 _amount) external; function claimRewards(address[] calldata _cTokenAddresses, bool _tradeForMorphoToken) external returns (uint256 claimedAmount); + /// @notice compound accrue interest function function updateP2PIndexes(address _poolTokenAddress) external; + + /// @notice aave accrue interest function + function updateIndexes(address _poolTokenAddress) external; } From 0ce23ba8d94c5f27acd28222c59943baf284804f Mon Sep 17 00:00:00 2001 From: Elliot Date: Thu, 19 Jan 2023 20:57:11 -0800 Subject: [PATCH 21/74] Deploy Euler and Morpho-Aave PCV Deposits --- test/proposals/Addresses.sol | 27 +++-- test/proposals/vips/vip15.sol | 2 +- test/proposals/vips/vip16.sol | 221 ++++++++++++++++++++++++++++------ test/proposals/vips/vip17.sol | 8 +- 4 files changed, 208 insertions(+), 50 deletions(-) diff --git a/test/proposals/Addresses.sol b/test/proposals/Addresses.sol index e0b446c8e..065cdef19 100644 --- a/test/proposals/Addresses.sol +++ b/test/proposals/Addresses.sol @@ -62,6 +62,11 @@ contract Addresses is Test { 0xF10d810De7F0Fbd455De30f8c43AbA56F253B73B ); + // ---------- EULER ADDRESSES ---------- + _addMainnet("EULER_MAIN", 0x27182842E098f60e3D576794A5bFFb0777E025d3); + _addMainnet("EUSDC", 0xEb91861f8A4e1C12333F42DCE8fB0Ecdc28dA716); + _addMainnet("EDAI", 0xe025E3ca2bE02316033184551D4d3Aa22024D9DC); + // ---------- ORACLE ADDRESSES ---------- _addMainnet( "ORACLE_PASS_THROUGH", @@ -126,6 +131,10 @@ contract Addresses is Test { 0x3d9819210A31b4961b30EF54bE2aeD79B9c9Cd3B ); + // ---------- Aave ADDRESSES ---------- + _addMainnet("ADAI", 0x028171bCA77440897B824Ca71D1c56caC55b68A3); + _addMainnet("AUSDC", 0xBcca60bB61934080951369a648Fb03DF4F96263C); + // ---------- Maker ADDRESSES ---------- _addMainnet( "MAKER_DAI_USDC_PSM", @@ -187,18 +196,20 @@ contract Addresses is Test { ); // ---------- MORPHO ADDRESSES ---------- - _addMainnet("MORPHO", 0x8888882f8f843896699869179fB6E4f7e3B58888); - _addMainnet("MORPHO_LENS", 0x930f1b46e1D081Ec1524efD95752bE3eCe51EF67); - // ---------- MAPLE ADDRESSES ---------- - _addMainnet("MPL_TOKEN", 0x33349B282065b0284d756F0577FB39c158F935e6); _addMainnet( - "MPL_ORTHOGONAL_POOL", - 0xFeBd6F15Df3B73DC4307B1d7E65D46413e710C27 + "MORPHO_COMPOUND", + 0x8888882f8f843896699869179fB6E4f7e3B58888 ); _addMainnet( - "MPL_ORTHOGONAL_REWARDS", - 0x7869D7a3B074b5fa484dc04798E254c9C06A5e90 + "MORPHO_LENS_COMPOUND", + 0x930f1b46e1D081Ec1524efD95752bE3eCe51EF67 + ); + + _addMainnet("MORPHO_AAVE", 0x777777c9898D384F785Ee44Acfe945efDFf5f3E0); + _addMainnet( + "MORPHO_LENS_AAVE", + 0x507fA343d0A90786d86C7cd885f5C49263A91FF4 ); } diff --git a/test/proposals/vips/vip15.sol b/test/proposals/vips/vip15.sol index 94da07cd2..945e8e048 100644 --- a/test/proposals/vips/vip15.sol +++ b/test/proposals/vips/vip15.sol @@ -28,7 +28,7 @@ contract vip15 is TimelockProposal { /// ------- poke morpho to update p2p indexes ------- _pushTimelockAction( - addresses.mainnet("MORPHO"), + addresses.mainnet("MORPHO_COMPOUND"), abi.encodeWithSignature( "updateP2PIndexes(address)", addresses.mainnet("CDAI") diff --git a/test/proposals/vips/vip16.sol b/test/proposals/vips/vip16.sol index 88b67e611..44a4ffad4 100644 --- a/test/proposals/vips/vip16.sol +++ b/test/proposals/vips/vip16.sol @@ -23,7 +23,9 @@ import {ERC20Allocator} from "@voltprotocol/pcv/ERC20Allocator.sol"; import {NonCustodialPSM} from "@voltprotocol/peg/NonCustodialPSM.sol"; import {MakerPCVSwapper} from "@voltprotocol/pcv/maker/MakerPCVSwapper.sol"; import {VoltSystemOracle} from "@voltprotocol/oracle/VoltSystemOracle.sol"; +import {EulerPCVDeposit} from "@voltprotocol/pcv/euler/EulerPCVDeposit.sol"; import {MorphoPCVDeposit} from "@voltprotocol/pcv/morpho/MorphoPCVDeposit.sol"; +import {MorphoAavePCVDeposit} from "@voltprotocol/pcv/morpho/MorphoAavePCVDeposit.sol"; import {PegStabilityModule} from "@voltprotocol/peg/PegStabilityModule.sol"; import {IPegStabilityModule} from "@voltprotocol/peg/IPegStabilityModule.sol"; import {ConstantPriceOracle} from "@voltprotocol/oracle/ConstantPriceOracle.sol"; @@ -149,31 +151,88 @@ contract vip16 is Proposal { /// PCV Deposits { - MorphoPCVDeposit morphoDaiPCVDeposit = new MorphoPCVDeposit( + /// Morpho Compound Deposits + MorphoPCVDeposit morphoCompoundDaiPCVDeposit = new MorphoPCVDeposit( addresses.mainnet("CORE"), addresses.mainnet("CDAI"), addresses.mainnet("DAI"), addresses.mainnet("COMP"), - addresses.mainnet("MORPHO"), - addresses.mainnet("MORPHO_LENS") + addresses.mainnet("MORPHO_COMPOUND"), + addresses.mainnet("MORPHO_LENS_COMPOUND") ); - MorphoPCVDeposit morphoUsdcPCVDeposit = new MorphoPCVDeposit( + MorphoPCVDeposit morphoCompoundUsdcPCVDeposit = new MorphoPCVDeposit( + addresses.mainnet("CORE"), + addresses.mainnet("CUSDC"), + addresses.mainnet("USDC"), + addresses.mainnet("COMP"), + addresses.mainnet("MORPHO_COMPOUND"), + addresses.mainnet("MORPHO_LENS_COMPOUND") + ); + + addresses.addMainnet( + "PCV_DEPOSIT_MORPHO_COMPOUND_DAI", + address(morphoCompoundDaiPCVDeposit) + ); + addresses.addMainnet( + "PCV_DEPOSIT_MORPHO_COMPOUND_USDC", + address(morphoCompoundUsdcPCVDeposit) + ); + } + { + /// Morpho Aave Deposits + MorphoAavePCVDeposit morphoAaveDaiPCVDeposit = new MorphoAavePCVDeposit( + addresses.mainnet("CORE"), + addresses.mainnet("ADAI"), + addresses.mainnet("DAI"), + address(0), + addresses.mainnet("MORPHO_AAVE"), + addresses.mainnet("MORPHO_LENS_AAVE") + ); + + MorphoAavePCVDeposit morphoAaveUsdcPCVDeposit = new MorphoAavePCVDeposit( + addresses.mainnet("CORE"), + addresses.mainnet("AUSDC"), + addresses.mainnet("USDC"), + address(0), + addresses.mainnet("MORPHO_AAVE"), + addresses.mainnet("MORPHO_LENS_AAVE") + ); + + addresses.addMainnet( + "PCV_DEPOSIT_MORPHO_AAVE_DAI", + address(morphoAaveDaiPCVDeposit) + ); + addresses.addMainnet( + "PCV_DEPOSIT_MORPHO_AAVE_USDC", + address(morphoAaveUsdcPCVDeposit) + ); + } + { + /// Euler Deposits + EulerPCVDeposit eulerDaiPCVDeposit = new EulerPCVDeposit( + addresses.mainnet("CORE"), + addresses.mainnet("EDAI"), + addresses.mainnet("EULER_MAIN"), + addresses.mainnet("DAI"), + address(0) + ); + + EulerPCVDeposit eulerUsdcPCVDeposit = new EulerPCVDeposit( addresses.mainnet("CORE"), - addresses.mainnet("CUSDC"), + addresses.mainnet("EUSDC"), + addresses.mainnet("EULER_MAIN"), addresses.mainnet("USDC"), - addresses.mainnet("COMP"), - addresses.mainnet("MORPHO"), - addresses.mainnet("MORPHO_LENS") + address(0) ); addresses.addMainnet( - "PCV_DEPOSIT_MORPHO_DAI", - address(morphoDaiPCVDeposit) + "PCV_DEPOSIT_EULER_DAI", + address(eulerDaiPCVDeposit) ); addresses.addMainnet( - "PCV_DEPOSIT_MORPHO_USDC", - address(morphoUsdcPCVDeposit) + "PCV_DEPOSIT_EULER_USDC", + address(eulerUsdcPCVDeposit) ); } @@ -217,7 +276,9 @@ contract vip16 is Proposal { IERC20(addresses.mainnet("USDC")), VOLT_FLOOR_PRICE_USDC, VOLT_CEILING_PRICE_USDC, - IPCVDeposit(addresses.mainnet("PCV_DEPOSIT_MORPHO_USDC")) + IPCVDeposit( + addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_USDC") + ) ); NonCustodialPSM daiNonCustodialPsm = new NonCustodialPSM( addresses.mainnet("CORE"), @@ -228,7 +289,9 @@ contract vip16 is Proposal { IERC20(addresses.mainnet("DAI")), VOLT_FLOOR_PRICE_DAI, VOLT_CEILING_PRICE_DAI, - IPCVDeposit(addresses.mainnet("PCV_DEPOSIT_MORPHO_DAI")) + IPCVDeposit( + addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_DAI") + ) ); ERC20Allocator allocator = new ERC20Allocator( addresses.mainnet("CORE") @@ -281,15 +344,27 @@ contract vip16 is Proposal { addresses.mainnet("CORE") ); - address[] memory pcvGuardianSafeAddresses = new address[](4); + address[] memory pcvGuardianSafeAddresses = new address[](8); pcvGuardianSafeAddresses[0] = addresses.mainnet( - "PCV_DEPOSIT_MORPHO_DAI" + "PCV_DEPOSIT_MORPHO_COMPOUND_DAI" ); pcvGuardianSafeAddresses[1] = addresses.mainnet( - "PCV_DEPOSIT_MORPHO_USDC" + "PCV_DEPOSIT_MORPHO_COMPOUND_USDC" ); pcvGuardianSafeAddresses[2] = addresses.mainnet("PSM_USDC"); pcvGuardianSafeAddresses[3] = addresses.mainnet("PSM_DAI"); + pcvGuardianSafeAddresses[4] = addresses.mainnet( + "PCV_DEPOSIT_EULER_DAI" + ); + pcvGuardianSafeAddresses[5] = addresses.mainnet( + "PCV_DEPOSIT_EULER_USDC" + ); + pcvGuardianSafeAddresses[6] = addresses.mainnet( + "PCV_DEPOSIT_MORPHO_AAVE_DAI" + ); + pcvGuardianSafeAddresses[7] = addresses.mainnet( + "PCV_DEPOSIT_MORPHO_AAVE_USDC" + ); PCVGuardian pcvGuardian = new PCVGuardian( addresses.mainnet("CORE"), @@ -414,11 +489,27 @@ contract vip16 is Proposal { core.createRole(VoltRoles.PCV_DEPOSIT, VoltRoles.GOVERNOR); core.grantRole( VoltRoles.PCV_DEPOSIT, - addresses.mainnet("PCV_DEPOSIT_MORPHO_DAI") + addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_DAI") ); core.grantRole( VoltRoles.PCV_DEPOSIT, - addresses.mainnet("PCV_DEPOSIT_MORPHO_USDC") + addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_USDC") + ); + core.grantRole( + VoltRoles.PCV_DEPOSIT, + addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_DAI") + ); + core.grantRole( + VoltRoles.PCV_DEPOSIT, + addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_USDC") + ); + core.grantRole( + VoltRoles.PCV_DEPOSIT, + addresses.mainnet("PCV_DEPOSIT_EULER_DAI") + ); + core.grantRole( + VoltRoles.PCV_DEPOSIT, + addresses.mainnet("PCV_DEPOSIT_EULER_USDC") ); core.grantPCVGuard(addresses.mainnet("EOA_1")); @@ -457,14 +548,18 @@ contract vip16 is Proposal { core.grantLocker(addresses.mainnet("PCV_ORACLE")); core.grantLocker(addresses.mainnet("PSM_DAI")); core.grantLocker(addresses.mainnet("PSM_USDC")); - core.grantLocker(addresses.mainnet("PCV_DEPOSIT_MORPHO_DAI")); - core.grantLocker(addresses.mainnet("PCV_DEPOSIT_MORPHO_USDC")); + core.grantLocker(addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_DAI")); + core.grantLocker(addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_USDC")); core.grantLocker(addresses.mainnet("GLOBAL_RATE_LIMITED_MINTER")); core.grantLocker(addresses.mainnet("GLOBAL_SYSTEM_EXIT_RATE_LIMITER")); core.grantLocker(addresses.mainnet("PCV_ROUTER")); core.grantLocker(addresses.mainnet("PCV_GUARDIAN")); core.grantLocker(addresses.mainnet("PSM_NONCUSTODIAL_DAI")); core.grantLocker(addresses.mainnet("PSM_NONCUSTODIAL_USDC")); + core.grantLocker(addresses.mainnet("PCV_DEPOSIT_EULER_DAI")); + core.grantLocker(addresses.mainnet("PCV_DEPOSIT_EULER_USDC")); + core.grantLocker(addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_DAI")); + core.grantLocker(addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_USDC")); core.grantMinter(addresses.mainnet("GLOBAL_RATE_LIMITED_MINTER")); @@ -481,21 +576,29 @@ contract vip16 is Proposal { ); allocator.connectDeposit( addresses.mainnet("PSM_USDC"), - addresses.mainnet("PCV_DEPOSIT_MORPHO_USDC") + addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_USDC") ); allocator.connectDeposit( addresses.mainnet("PSM_DAI"), - addresses.mainnet("PCV_DEPOSIT_MORPHO_DAI") + addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_DAI") ); /// Configure PCV Oracle - address[] memory venues = new address[](2); - venues[0] = addresses.mainnet("PCV_DEPOSIT_MORPHO_DAI"); - venues[1] = addresses.mainnet("PCV_DEPOSIT_MORPHO_USDC"); - - address[] memory oracles = new address[](2); + address[] memory venues = new address[](6); + venues[0] = addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_DAI"); + venues[1] = addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_USDC"); + venues[2] = addresses.mainnet("PCV_DEPOSIT_EULER_DAI"); + venues[3] = addresses.mainnet("PCV_DEPOSIT_EULER_USDC"); + venues[4] = addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_DAI"); + venues[5] = addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_USDC"); + + address[] memory oracles = new address[](6); oracles[0] = addresses.mainnet("ORACLE_CONSTANT_DAI"); oracles[1] = addresses.mainnet("ORACLE_CONSTANT_USDC"); + oracles[2] = addresses.mainnet("ORACLE_CONSTANT_DAI"); + oracles[3] = addresses.mainnet("ORACLE_CONSTANT_USDC"); + oracles[4] = addresses.mainnet("ORACLE_CONSTANT_DAI"); + oracles[5] = addresses.mainnet("ORACLE_CONSTANT_USDC"); pcvOracle.addVenues(venues, oracles); @@ -593,13 +696,41 @@ contract vip16 is Proposal { ); assertEq( address( - CoreRefV2(addresses.mainnet("PCV_DEPOSIT_MORPHO_DAI")).core() + CoreRefV2(addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_DAI")) + .core() ), address(core) ); assertEq( address( - CoreRefV2(addresses.mainnet("PCV_DEPOSIT_MORPHO_USDC")).core() + CoreRefV2(addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_USDC")) + .core() + ), + address(core) + ); + assertEq( + address( + CoreRefV2(addresses.mainnet("PCV_DEPOSIT_EULER_DAI")).core() + ), + address(core) + ); + assertEq( + address( + CoreRefV2(addresses.mainnet("PCV_DEPOSIT_EULER_USDC")).core() + ), + address(core) + ); + assertEq( + address( + CoreRefV2(addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_DAI")) + .core() + ), + address(core) + ); + assertEq( + address( + CoreRefV2(addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_USDC")) + .core() ), address(core) ); @@ -690,13 +821,13 @@ contract vip16 is Proposal { // psm allocator assertEq( allocator.pcvDepositToPSM( - addresses.mainnet("PCV_DEPOSIT_MORPHO_USDC") + addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_USDC") ), addresses.mainnet("PSM_USDC") ); assertEq( allocator.pcvDepositToPSM( - addresses.mainnet("PCV_DEPOSIT_MORPHO_DAI") + addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_DAI") ), addresses.mainnet("PSM_DAI") ); @@ -710,14 +841,30 @@ contract vip16 is Proposal { assertEq(psmToken2, addresses.mainnet("USDC")); // pcv oracle - assertEq(pcvOracle.getVenues().length, 2); + assertEq(pcvOracle.getVenues().length, 6); assertEq( pcvOracle.getVenues()[0], - addresses.mainnet("PCV_DEPOSIT_MORPHO_DAI") + addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_DAI") ); assertEq( pcvOracle.getVenues()[1], - addresses.mainnet("PCV_DEPOSIT_MORPHO_USDC") + addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_USDC") + ); + assertEq( + pcvOracle.getVenues()[2], + addresses.mainnet("PCV_DEPOSIT_EULER_DAI") + ); + assertEq( + pcvOracle.getVenues()[3], + addresses.mainnet("PCV_DEPOSIT_EULER_USDC") + ); + assertEq( + pcvOracle.getVenues()[4], + addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_DAI") + ); + assertEq( + pcvOracle.getVenues()[5], + addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_USDC") ); // pcv router @@ -745,12 +892,12 @@ contract vip16 is Proposal { /// compound bad debt sentinel assertTrue( badDebtSentinel.isCompoundPcvDeposit( - addresses.mainnet("PCV_DEPOSIT_MORPHO_USDC") + addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_USDC") ) ); assertTrue( badDebtSentinel.isCompoundPcvDeposit( - addresses.mainnet("PCV_DEPOSIT_MORPHO_DAI") + addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_DAI") ) ); assertEq( diff --git a/test/proposals/vips/vip17.sol b/test/proposals/vips/vip17.sol index 4bab0f566..1cf0eccf1 100644 --- a/test/proposals/vips/vip17.sol +++ b/test/proposals/vips/vip17.sol @@ -63,7 +63,7 @@ contract vip17 is MultisigProposal { addresses.mainnet("USDC"), abi.encodeWithSignature( "transfer(address,uint256)", - addresses.mainnet("PCV_DEPOSIT_MORPHO_USDC"), + addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_USDC"), PCV_USDC - PSM_LIQUID_RESERVE * 1e6 ), "Send Protocol USDC to Morpho-Compound USDC Deposit" @@ -73,7 +73,7 @@ contract vip17 is MultisigProposal { addresses.mainnet("SYSTEM_ENTRY"), abi.encodeWithSignature( "deposit(address)", - addresses.mainnet("PCV_DEPOSIT_MORPHO_USDC") + addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_USDC") ), "Deposit USDC" ); @@ -92,7 +92,7 @@ contract vip17 is MultisigProposal { addresses.mainnet("DAI"), abi.encodeWithSignature( "transfer(address,uint256)", - addresses.mainnet("PCV_DEPOSIT_MORPHO_DAI"), + addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_DAI"), PCV_DAI - PSM_LIQUID_RESERVE * 1e18 ), "Send Protocol DAI to Morpho-Compound DAI Deposit" @@ -102,7 +102,7 @@ contract vip17 is MultisigProposal { addresses.mainnet("SYSTEM_ENTRY"), abi.encodeWithSignature( "deposit(address)", - addresses.mainnet("PCV_DEPOSIT_MORPHO_DAI") + addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_DAI") ), "Deposit DAI" ); From 98b43cfc4b1efd87d53c63811a3c56ef82c24168 Mon Sep 17 00:00:00 2001 From: Elliot Date: Thu, 19 Jan 2023 20:57:40 -0800 Subject: [PATCH 22/74] remove compound pcv deposit --- src/pcv/morpho/MorphoCompoundPCVDeposit.sol | 269 -------------------- 1 file changed, 269 deletions(-) delete mode 100644 src/pcv/morpho/MorphoCompoundPCVDeposit.sol diff --git a/src/pcv/morpho/MorphoCompoundPCVDeposit.sol b/src/pcv/morpho/MorphoCompoundPCVDeposit.sol deleted file mode 100644 index 452c75d92..000000000 --- a/src/pcv/morpho/MorphoCompoundPCVDeposit.sol +++ /dev/null @@ -1,269 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.13; - -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; -import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {Address} from "@openzeppelin/contracts/utils/Address.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -import {ILens} from "@voltprotocol/pcv/morpho/ILens.sol"; -import {IMorpho} from "@voltprotocol/pcv/morpho/IMorpho.sol"; -import {ICToken} from "@voltprotocol/pcv/morpho/ICompound.sol"; -import {Constants} from "@voltprotocol/Constants.sol"; -import {CoreRefV2} from "@voltprotocol/refs/CoreRefV2.sol"; -import {CoreRefV2} from "@voltprotocol/refs/CoreRefV2.sol"; -import {PCVDeposit} from "@voltprotocol/pcv/PCVDeposit.sol"; -import {IPCVDepositV2} from "@voltprotocol/pcv/IPCVDepositV2.sol"; - -/// @title abstract contract for a PCV Deposit that handles ERC-20 tokens using a PCV Controller -/// @author Elliot Friedman -abstract contract PCVDepositV2 is IPCVDepositV2, CoreRefV2 { - using SafeERC20 for IERC20; - using SafeCast for *; - - /// @notice reference to underlying token - address public immutable override rewardToken; - - /// @notice reference to underlying token - address public immutable override token; - - /// ------------------------------------------ - /// ------------- State Variables ------------ - /// ------------------------------------------ - - /// @notice track the last amount of PCV recorded in the contract - /// this is always out of date, except when accrue() is called - /// in the same block or transaction. This means the value is stale - /// most of the time. - uint128 public lastRecordedBalance; - - /// @notice track the last amount of profits earned by the contract - /// this is always out of date, except when accrue() is called - /// in the same block or transaction. This means the value is stale - /// most of the time. - int128 public lastRecordedProfits; - - constructor(address _token, address _rewardToken) { - token = _token; - rewardToken = _rewardToken; - } - - /// ------------------------------------------ - /// ----------- Permissionless API ----------- - /// ------------------------------------------ - - /// @notice deposit ERC-20 tokens to Morpho-Compound - /// non-reentrant to block malicious reentrant state changes - /// to the lastRecordedBalance variable - function deposit() public whenNotPaused globalLock(2) { - /// ------ Check ------ - - uint256 amount = IERC20(token).balanceOf(address(this)); - if (amount == 0) { - /// no op to prevent revert on empty deposit - return; - } - - int256 startingRecordedBalance = lastRecordedBalance.toInt256(); - - /// ------ Effects ------ - - /// compute profit from interest accrued and emit an event - /// if any profits or losses are realized - int256 profit = _recordPNL(); - - /// increment tracked recorded amount - /// this will be off by a hair, after a single block - /// negative delta turns to positive delta (assuming no loss). - lastRecordedBalance += uint128(amount); - - /// ------ Interactions ------ - - /// approval and deposit into underlying venue - _supply(amount); - - /// ------ Update Internal Accounting ------ - - int256 endingRecordedBalance = balance().toInt256(); - - _pcvOracleHook(endingRecordedBalance - startingRecordedBalance, profit); - - emit Deposit(msg.sender, amount); - } - - /// @notice claim COMP rewards for supplying to Morpho. - /// Does not require reentrancy lock as no smart contract state is mutated - /// in this function. - function harvest() external globalLock(2) { - uint256 claimedAmount = _claim(); - - emit Harvest(rewardToken, int256(claimedAmount), block.timestamp); - } - - /// @notice function that emits an event tracking profits and losses - /// since the last contract interaction - /// then writes the current amount of PCV tracked in this contract - /// to lastRecordedBalance - /// @return the amount deposited after adding accrued interest or realizing losses - function accrue() external globalLock(2) whenNotPaused returns (uint256) { - int256 profit = _recordPNL(); /// update deposit amount and fire harvest event - - /// if any amount of PCV is withdrawn and no gains, delta is negative - _pcvOracleHook(profit, profit); - - return lastRecordedBalance; /// return updated pcv amount - } - - /// ------------------------------------------ - /// ------------ Permissioned API ------------ - /// ------------------------------------------ - - /// @notice withdraw tokens from the PCV allocation - /// non-reentrant as state changes and external calls are made - /// @param to the address PCV will be sent to - /// @param amount of tokens withdrawn - function withdraw( - address to, - uint256 amount - ) external onlyPCVController globalLock(2) { - int256 profit = _withdraw(to, amount, true); - - /// if any amount of PCV is withdrawn and no gains, delta is negative - _pcvOracleHook(-(amount.toInt256()) + profit, profit); - } - - /// @notice withdraw all tokens from Morpho - /// non-reentrant as state changes and external calls are made - /// @param to the address PCV will be sent to - function withdrawAll(address to) external onlyPCVController globalLock(2) { - /// compute profit from interest accrued and emit an event - int256 profit = _recordPNL(); - - int256 recordedBalance = lastRecordedBalance.toInt256(); - - /// withdraw last recorded amount as this was updated in record pnl - _withdraw(to, lastRecordedBalance, false); - - /// all PCV withdrawn, send call in with amount withdrawn negative if any amount is withdrawn - _pcvOracleHook(-recordedBalance, profit); - } - - /// @notice withdraw ERC20 from the contract - /// @param tokenAddress address of the ERC20 to send - /// @param to address destination of the ERC20 - /// @param amount quantity of ERC20 to send - /// Calling this function will lead to incorrect - /// accounting in a PCV deposit that tracks - /// profits and or last recorded balance. - /// If a deposit records PNL, only use this - /// function in an emergency. - function withdrawERC20( - address tokenAddress, - address to, - uint256 amount - ) public virtual override onlyPCVController { - IERC20(tokenAddress).safeTransfer(to, amount); - emit WithdrawERC20(msg.sender, tokenAddress, to, amount); - } - - /// ------------- Internal Helpers ------------- - - /// @notice records how much profit or loss has been accrued - /// since the last call and emits an event with all profit or loss received. - /// Updates the lastRecordedBalance to include all realized profits or losses. - /// @return profit accumulated since last _recordPNL() call. - function _recordPNL() internal returns (int256) { - /// first accrue interest in the underlying venue - _accrueUnderlying(); - - /// ------ Check ------ - - /// then get the current balance from the market - uint256 currentBalance = balance(); - - /// save gas if contract has no balance - /// if cost basis is 0 and last recorded balance is 0 - /// there is no profit or loss to record and no reason - /// to update lastRecordedBalance - if (currentBalance == 0 && lastRecordedBalance == 0) { - return 0; - } - - /// currentBalance should always be greater than or equal to - /// the deposited amount, except on the same block a deposit occurs, or a loss event in morpho - /// SLOAD - uint128 _lastRecordedBalance = lastRecordedBalance; - - /// Compute profit - int128 profit = int128(int256(currentBalance)) - - int128(_lastRecordedBalance); - - int128 _lastRecordedProfits = lastRecordedProfits + profit; - - /// ------ Effects ------ - - /// SSTORE: record new amounts - lastRecordedProfits = _lastRecordedProfits; - lastRecordedBalance = uint128(currentBalance); - - /// profit is in underlying token - emit Harvest(token, int256(profit), block.timestamp); - - return profit; - } - - /// @notice helper avoid repeated code in withdraw and withdrawAll - /// anytime this function is called it is by an external function in this smart contract - /// with a reentrancy guard. This ensures lastRecordedBalance never desynchronizes. - /// Morpho is assumed to be a loss-less venue. over the course of less than 1 block, - /// it is possible to lose funds. However, after 1 block, deposits are expected to always - /// be in profit at least with current interest rates around 0.8% natively on Compound, - /// ignoring all COMP and Morpho rewards. - /// @param to recipient of withdraw funds - /// @param amount to withdraw - /// @param recordPnl whether or not to record PnL. Set to false in withdrawAll - /// as the function _recordPNL() is already called before _withdraw - function _withdraw( - address to, - uint256 amount, - bool recordPnl - ) private returns (int256 profit) { - /// ------ Effects ------ - - if (recordPnl) { - /// compute profit from interest accrued and emit a Harvest event - profit = _recordPNL(); - } - - /// update last recorded balance amount - /// if more than is owned is withdrawn, this line will revert - /// this line of code is both a check, and an effect - lastRecordedBalance -= uint128(amount); - - /// ------ Interactions ------ - - /// remove funds from underlying venue - _withdrawAndTransfer(amount, to); - - emit Withdrawal(msg.sender, to, amount); - } - - /// ------------- Virtual Functions ------------- - - /// @notice get balance in the underlying market. - /// @return current balance of deposit - function balance() public view virtual override returns (uint256); - - /// @dev accrue interest in the underlying market. - function _accrueUnderlying() internal virtual; - - /// @dev withdraw from the underlying market. - function _withdrawAndTransfer(uint256 amount, address to) internal virtual; - - /// @dev deposit in the underlying market. - function _supply(uint256 amount) internal virtual; - - /// @dev claim rewards from the underlying market. - /// returns amount of reward tokens claimed - function _claim() internal virtual returns (uint256); -} From 645f59b628c6466115e2eaee8423d0c09511597f Mon Sep 17 00:00:00 2001 From: Elliot Date: Thu, 19 Jan 2023 20:58:13 -0800 Subject: [PATCH 23/74] import clean up --- src/pcv/morpho/MorphoPCVDeposit.sol | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/pcv/morpho/MorphoPCVDeposit.sol b/src/pcv/morpho/MorphoPCVDeposit.sol index 4172820d8..3d2ffc513 100644 --- a/src/pcv/morpho/MorphoPCVDeposit.sol +++ b/src/pcv/morpho/MorphoPCVDeposit.sol @@ -2,14 +2,11 @@ pragma solidity =0.8.13; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {ILens} from "@voltprotocol/pcv/morpho/ILens.sol"; -import {ICToken} from "@voltprotocol/pcv/morpho/ICompound.sol"; import {IMorpho} from "@voltprotocol/pcv/morpho/IMorpho.sol"; import {CoreRefV2} from "@voltprotocol/refs/CoreRefV2.sol"; -import {Constants} from "@voltprotocol/Constants.sol"; import {PCVDepositV2} from "@voltprotocol/pcv/PCVDepositV2.sol"; /// @notice PCV Deposit for Morpho-Compound V2. @@ -37,7 +34,6 @@ import {PCVDepositV2} from "@voltprotocol/pcv/PCVDepositV2.sol"; /// the user a balance. contract MorphoPCVDeposit is PCVDepositV2 { using SafeERC20 for IERC20; - using SafeCast for *; /// ------------------------------------------ /// ---------- Immutables/Constant ----------- @@ -87,11 +83,6 @@ contract MorphoPCVDeposit is PCVDepositV2 { return totalSupplied; } - /// @notice returns the underlying token of this deposit - function balanceReportedIn() external view returns (address) { - return token; - } - /// ------------------------------------------ /// ------------- Helper Methods ------------- /// ------------------------------------------ From dd1bd799866a515370fd66613b4038ffe75117ba Mon Sep 17 00:00:00 2001 From: Elliot Date: Thu, 19 Jan 2023 20:58:41 -0800 Subject: [PATCH 24/74] change unit test morpho address label --- test/unit/pcv/morpho/MorphoCompoundPCVDeposit.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/pcv/morpho/MorphoCompoundPCVDeposit.t.sol b/test/unit/pcv/morpho/MorphoCompoundPCVDeposit.t.sol index b1c1afc66..615d7602f 100644 --- a/test/unit/pcv/morpho/MorphoCompoundPCVDeposit.t.sol +++ b/test/unit/pcv/morpho/MorphoCompoundPCVDeposit.t.sol @@ -83,7 +83,7 @@ contract UnitTestMorphoPCVDeposit is Test { core.setGlobalReentrancyLock(lock); vm.stopPrank(); - vm.label(address(morpho), "Morpho"); + vm.label(address(morpho), "MORPHO_COMPOUND"); vm.label(address(token), "Token"); vm.label(address(morphoDeposit), "MorphoDeposit"); From 00e7844ee198d984e43fef5d3d35e1b14273a461 Mon Sep 17 00:00:00 2001 From: Elliot Date: Thu, 19 Jan 2023 21:04:37 -0800 Subject: [PATCH 25/74] Rename MorphoPCVDeposit to MorphoCompoundPCVDeposit --- ...posit.sol => MorphoCompoundPCVDeposit.sol} | 2 +- ...egrationTestMorphoCompoundPCVDeposit.t.sol | 12 +++++----- .../InvariantTestMorphoPCVDeposit.t.sol | 18 +++++++-------- test/mock/MockMorphoMaliciousReentrancy.sol | 8 +++---- test/proposals/vips/vip16.sol | 20 ++++++++-------- .../compound/CompoundBadDebtSentinel.t.sol | 6 ++--- .../pcv/morpho/MorphoCompoundPCVDeposit.t.sol | 23 +++++++++++-------- test/unit/system/System.t.sol | 4 ++-- 8 files changed, 48 insertions(+), 45 deletions(-) rename src/pcv/morpho/{MorphoPCVDeposit.sol => MorphoCompoundPCVDeposit.sol} (99%) diff --git a/src/pcv/morpho/MorphoPCVDeposit.sol b/src/pcv/morpho/MorphoCompoundPCVDeposit.sol similarity index 99% rename from src/pcv/morpho/MorphoPCVDeposit.sol rename to src/pcv/morpho/MorphoCompoundPCVDeposit.sol index 3d2ffc513..0b36c34b9 100644 --- a/src/pcv/morpho/MorphoPCVDeposit.sol +++ b/src/pcv/morpho/MorphoCompoundPCVDeposit.sol @@ -32,7 +32,7 @@ import {PCVDepositV2} from "@voltprotocol/pcv/PCVDepositV2.sol"; /// and protocol engineers are forced to choose who to round in favor of. /// Engineers must round in favor of the protocol to avoid deposits of 0 giving /// the user a balance. -contract MorphoPCVDeposit is PCVDepositV2 { +contract MorphoCompoundPCVDeposit is PCVDepositV2 { using SafeERC20 for IERC20; /// ------------------------------------------ diff --git a/test/integration/IntegrationTestMorphoCompoundPCVDeposit.t.sol b/test/integration/IntegrationTestMorphoCompoundPCVDeposit.t.sol index a45681253..73b018480 100644 --- a/test/integration/IntegrationTestMorphoCompoundPCVDeposit.t.sol +++ b/test/integration/IntegrationTestMorphoCompoundPCVDeposit.t.sol @@ -9,12 +9,12 @@ import {CoreV2} from "@voltprotocol/core/CoreV2.sol"; import {getCoreV2} from "@test/unit/utils/Fixtures.sol"; import {PCVGuardian} from "@voltprotocol/pcv/PCVGuardian.sol"; import {SystemEntry} from "@voltprotocol/entry/SystemEntry.sol"; -import {MorphoPCVDeposit} from "@voltprotocol/pcv/morpho/MorphoPCVDeposit.sol"; +import {MorphoCompoundPCVDeposit} from "@voltprotocol/pcv/morpho/MorphoCompoundPCVDeposit.sol"; import {PegStabilityModule} from "@voltprotocol/peg/PegStabilityModule.sol"; import {TestAddresses as addresses} from "@test/unit/utils/TestAddresses.sol"; import {IGlobalReentrancyLock, GlobalReentrancyLock} from "@voltprotocol/core/GlobalReentrancyLock.sol"; -contract IntegrationTestMorphoPCVDeposit is Test { +contract IntegrationTestMorphoCompoundPCVDeposit is Test { using SafeCast for *; // Constant addresses @@ -33,8 +33,8 @@ contract IntegrationTestMorphoPCVDeposit is Test { CoreV2 private core; SystemEntry public entry; GlobalReentrancyLock private lock; - MorphoPCVDeposit private daiDeposit; - MorphoPCVDeposit private usdcDeposit; + MorphoCompoundPCVDeposit private daiDeposit; + MorphoCompoundPCVDeposit private usdcDeposit; PCVGuardian private pcvGuardian; @@ -53,7 +53,7 @@ contract IntegrationTestMorphoPCVDeposit is Test { function setUp() public { core = getCoreV2(); lock = new GlobalReentrancyLock(address(core)); - daiDeposit = new MorphoPCVDeposit( + daiDeposit = new MorphoCompoundPCVDeposit( address(core), CDAI, DAI, @@ -62,7 +62,7 @@ contract IntegrationTestMorphoPCVDeposit is Test { MORPHO_LENS ); - usdcDeposit = new MorphoPCVDeposit( + usdcDeposit = new MorphoCompoundPCVDeposit( address(core), CUSDC, USDC, diff --git a/test/invariant/InvariantTestMorphoPCVDeposit.t.sol b/test/invariant/InvariantTestMorphoPCVDeposit.t.sol index f040d9e48..42fff9f26 100644 --- a/test/invariant/InvariantTestMorphoPCVDeposit.t.sol +++ b/test/invariant/InvariantTestMorphoPCVDeposit.t.sol @@ -14,7 +14,7 @@ import {PCVGuardian} from "@voltprotocol/pcv/PCVGuardian.sol"; import {SystemEntry} from "@voltprotocol/entry/SystemEntry.sol"; import {MockPCVOracle} from "@test/mock/MockPCVOracle.sol"; import {InvariantTest} from "@test/invariant/InvariantTest.sol"; -import {MorphoPCVDeposit} from "@voltprotocol/pcv/morpho/MorphoPCVDeposit.sol"; +import {MorphoCompoundPCVDeposit} from "@voltprotocol/pcv/morpho/MorphoCompoundPCVDeposit.sol"; import {TestAddresses as addresses} from "@test/unit/utils/TestAddresses.sol"; import {IGlobalReentrancyLock, GlobalReentrancyLock} from "@voltprotocol/core/GlobalReentrancyLock.sol"; @@ -22,7 +22,7 @@ import {IGlobalReentrancyLock, GlobalReentrancyLock} from "@voltprotocol/core/Gl /// will not run invariant tests /// @dev Modified from Solmate ERC20 Invariant Test (https://github.com/transmissions11/solmate/blob/main/src/test/ERC20.t.sol) -contract InvariantTestMorphoPCVDeposit is Test, InvariantTest { +contract InvariantTestMorphoCompoundPCVDeposit is Test, InvariantTest { using SafeCast for *; /// TODO add invariant test for profit tracking @@ -34,15 +34,15 @@ contract InvariantTestMorphoPCVDeposit is Test, InvariantTest { PCVGuardian public pcvGuardian; MockPCVOracle public pcvOracle; IGlobalReentrancyLock private lock; - MorphoPCVDepositTest public morphoTest; - MorphoPCVDeposit public morphoDeposit; + MorphoCompoundPCVDepositTest public morphoTest; + MorphoCompoundPCVDeposit public morphoDeposit; function setUp() public { core = getCoreV2(); token = new MockERC20(); pcvOracle = new MockPCVOracle(); morpho = new MockMorpho(IERC20(address(token))); - morphoDeposit = new MorphoPCVDeposit( + morphoDeposit = new MorphoCompoundPCVDeposit( address(core), address(morpho), address(token), @@ -64,7 +64,7 @@ contract InvariantTestMorphoPCVDeposit is Test, InvariantTest { ); entry = new SystemEntry(address(core)); - morphoTest = new MorphoPCVDepositTest( + morphoTest = new MorphoCompoundPCVDepositTest( morphoDeposit, token, morpho, @@ -114,17 +114,17 @@ contract InvariantTestMorphoPCVDeposit is Test, InvariantTest { } } -contract MorphoPCVDepositTest is Test { +contract MorphoCompoundPCVDepositTest is Test { uint256 public totalDeposited; MockERC20 public token; MockMorpho public morpho; SystemEntry public entry; PCVGuardian public pcvGuardian; - MorphoPCVDeposit public morphoDeposit; + MorphoCompoundPCVDeposit public morphoDeposit; constructor( - MorphoPCVDeposit _morphoDeposit, + MorphoCompoundPCVDeposit _morphoDeposit, MockERC20 _token, MockMorpho _morpho, SystemEntry _entry, diff --git a/test/mock/MockMorphoMaliciousReentrancy.sol b/test/mock/MockMorphoMaliciousReentrancy.sol index ecc015bdb..8fb8bd15c 100644 --- a/test/mock/MockMorphoMaliciousReentrancy.sol +++ b/test/mock/MockMorphoMaliciousReentrancy.sol @@ -2,12 +2,12 @@ pragma solidity 0.8.13; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import {MorphoPCVDeposit} from "@voltprotocol/pcv/morpho/MorphoPCVDeposit.sol"; +import {MorphoCompoundPCVDeposit} from "@voltprotocol/pcv/morpho/MorphoCompoundPCVDeposit.sol"; contract MockMorphoMaliciousReentrancy { IERC20 public immutable token; mapping(address => uint256) public balances; - MorphoPCVDeposit public morphoCompoundPCVDeposit; + MorphoCompoundPCVDeposit public morphoCompoundPCVDeposit; constructor(IERC20 _token) { token = _token; @@ -17,8 +17,8 @@ contract MockMorphoMaliciousReentrancy { return address(token); } - function setMorphoPCVDeposit(address deposit) external { - morphoCompoundPCVDeposit = MorphoPCVDeposit(deposit); + function setMorphoCompoundPCVDeposit(address deposit) external { + morphoCompoundPCVDeposit = MorphoCompoundPCVDeposit(deposit); } function withdraw(address, uint256) external { diff --git a/test/proposals/vips/vip16.sol b/test/proposals/vips/vip16.sol index 44a4ffad4..bd7334b4b 100644 --- a/test/proposals/vips/vip16.sol +++ b/test/proposals/vips/vip16.sol @@ -24,7 +24,7 @@ import {NonCustodialPSM} from "@voltprotocol/peg/NonCustodialPSM.sol"; import {MakerPCVSwapper} from "@voltprotocol/pcv/maker/MakerPCVSwapper.sol"; import {VoltSystemOracle} from "@voltprotocol/oracle/VoltSystemOracle.sol"; import {EulerPCVDeposit} from "@voltprotocol/pcv/euler/EulerPCVDeposit.sol"; -import {MorphoPCVDeposit} from "@voltprotocol/pcv/morpho/MorphoPCVDeposit.sol"; +import {MorphoCompoundPCVDeposit} from "@voltprotocol/pcv/morpho/MorphoCompoundPCVDeposit.sol"; import {MorphoAavePCVDeposit} from "@voltprotocol/pcv/morpho/MorphoAavePCVDeposit.sol"; import {PegStabilityModule} from "@voltprotocol/peg/PegStabilityModule.sol"; import {IPegStabilityModule} from "@voltprotocol/peg/IPegStabilityModule.sol"; @@ -152,16 +152,16 @@ contract vip16 is Proposal { /// PCV Deposits { /// Morpho Compound Deposits - MorphoPCVDeposit morphoCompoundDaiPCVDeposit = new MorphoPCVDeposit( - addresses.mainnet("CORE"), - addresses.mainnet("CDAI"), - addresses.mainnet("DAI"), - addresses.mainnet("COMP"), - addresses.mainnet("MORPHO_COMPOUND"), - addresses.mainnet("MORPHO_LENS_COMPOUND") - ); + MorphoCompoundPCVDeposit morphoCompoundDaiPCVDeposit = new MorphoCompoundPCVDeposit( + addresses.mainnet("CORE"), + addresses.mainnet("CDAI"), + addresses.mainnet("DAI"), + addresses.mainnet("COMP"), + addresses.mainnet("MORPHO_COMPOUND"), + addresses.mainnet("MORPHO_LENS_COMPOUND") + ); - MorphoPCVDeposit morphoCompoundUsdcPCVDeposit = new MorphoPCVDeposit( + MorphoCompoundPCVDeposit morphoCompoundUsdcPCVDeposit = new MorphoCompoundPCVDeposit( addresses.mainnet("CORE"), addresses.mainnet("CUSDC"), addresses.mainnet("USDC"), diff --git a/test/unit/pcv/compound/CompoundBadDebtSentinel.t.sol b/test/unit/pcv/compound/CompoundBadDebtSentinel.t.sol index ed8ef05f9..d2c749510 100644 --- a/test/unit/pcv/compound/CompoundBadDebtSentinel.t.sol +++ b/test/unit/pcv/compound/CompoundBadDebtSentinel.t.sol @@ -16,7 +16,7 @@ import {GenericCallMock} from "@test/mock/GenericCallMock.sol"; import {MockPCVDepositV3} from "@test/mock/MockPCVDepositV3.sol"; import {MockERC20, IERC20} from "@test/mock/MockERC20.sol"; import {CompoundBadDebtSentinel} from "@voltprotocol/pcv/compound/CompoundBadDebtSentinel.sol"; -import {MorphoPCVDeposit} from "@voltprotocol/pcv/morpho/MorphoPCVDeposit.sol"; +import {MorphoCompoundPCVDeposit} from "@voltprotocol/pcv/morpho/MorphoCompoundPCVDeposit.sol"; import {TestAddresses as addresses} from "@test/unit/utils/TestAddresses.sol"; import {MockMorphoMaliciousReentrancy} from "@test/mock/MockMorphoMaliciousReentrancy.sol"; import {IGlobalReentrancyLock, GlobalReentrancyLock} from "@voltprotocol/core/GlobalReentrancyLock.sol"; @@ -40,7 +40,7 @@ contract UnitTestCompoundBadDebtSentinel is Test { PCVGuardian private pcvGuardian; GenericCallMock private comptroller; MockPCVDepositV3 private safeAddress; - MorphoPCVDeposit private morphoDeposit; + MorphoCompoundPCVDeposit private morphoDeposit; CompoundBadDebtSentinel private badDebtSentinel; uint256 public badDebtThreshold = 1_000_000e18; @@ -66,7 +66,7 @@ contract UnitTestCompoundBadDebtSentinel is Test { comptroller = new GenericCallMock(); safeAddress = new MockPCVDepositV3(address(core), address(token)); - morphoDeposit = new MorphoPCVDeposit( + morphoDeposit = new MorphoCompoundPCVDeposit( address(core), address(morpho), address(token), diff --git a/test/unit/pcv/morpho/MorphoCompoundPCVDeposit.t.sol b/test/unit/pcv/morpho/MorphoCompoundPCVDeposit.t.sol index 615d7602f..f8bf2b1ea 100644 --- a/test/unit/pcv/morpho/MorphoCompoundPCVDeposit.t.sol +++ b/test/unit/pcv/morpho/MorphoCompoundPCVDeposit.t.sol @@ -13,12 +13,12 @@ import {PCVGuardian} from "@voltprotocol/pcv/PCVGuardian.sol"; import {getCoreV2} from "@test/unit/utils/Fixtures.sol"; import {SystemEntry} from "@voltprotocol/entry/SystemEntry.sol"; import {MockERC20, IERC20} from "@test/mock/MockERC20.sol"; -import {MorphoPCVDeposit} from "@voltprotocol/pcv/morpho/MorphoPCVDeposit.sol"; +import {MorphoCompoundPCVDeposit} from "@voltprotocol/pcv/morpho/MorphoCompoundPCVDeposit.sol"; import {TestAddresses as addresses} from "@test/unit/utils/TestAddresses.sol"; import {MockMorphoMaliciousReentrancy} from "@test/mock/MockMorphoMaliciousReentrancy.sol"; import {IGlobalReentrancyLock, GlobalReentrancyLock} from "@voltprotocol/core/GlobalReentrancyLock.sol"; -contract UnitTestMorphoPCVDeposit is Test { +contract UnitTestMorphoCompoundPCVDeposit is Test { using SafeCast for *; event Deposit(address indexed _from, uint256 _amount); @@ -35,7 +35,7 @@ contract UnitTestMorphoPCVDeposit is Test { SystemEntry public entry; MockMorpho private morpho; PCVGuardian private pcvGuardian; - MorphoPCVDeposit private morphoDeposit; + MorphoCompoundPCVDeposit private morphoDeposit; MockMorphoMaliciousReentrancy private maliciousMorpho; /// @notice token to deposit @@ -56,7 +56,7 @@ contract UnitTestMorphoPCVDeposit is Test { IERC20(address(token)) ); - morphoDeposit = new MorphoPCVDeposit( + morphoDeposit = new MorphoCompoundPCVDeposit( address(core), address(morpho), address(token), @@ -87,7 +87,7 @@ contract UnitTestMorphoPCVDeposit is Test { vm.label(address(token), "Token"); vm.label(address(morphoDeposit), "MorphoDeposit"); - maliciousMorpho.setMorphoPCVDeposit(address(morphoDeposit)); + maliciousMorpho.setMorphoCompoundPCVDeposit(address(morphoDeposit)); } function testSetup() public { @@ -238,7 +238,8 @@ contract UnitTestMorphoPCVDeposit is Test { token.mint(address(morphoDeposit), amount); entry.deposit(address(morphoDeposit)); - MorphoPCVDeposit.Call[] memory calls = new MorphoPCVDeposit.Call[](1); + MorphoCompoundPCVDeposit.Call[] + memory calls = new MorphoCompoundPCVDeposit.Call[](1); calls[0].callData = abi.encodeWithSignature( "withdraw(address,uint256)", address(this), @@ -257,7 +258,8 @@ contract UnitTestMorphoPCVDeposit is Test { vm.assume(amount != 0); token.mint(address(morphoDeposit), amount); - MorphoPCVDeposit.Call[] memory calls = new MorphoPCVDeposit.Call[](2); + MorphoCompoundPCVDeposit.Call[] + memory calls = new MorphoCompoundPCVDeposit.Call[](2); calls[0].callData = abi.encodeWithSignature( "approve(address,uint256)", address(morphoDeposit.morpho()), @@ -303,7 +305,8 @@ contract UnitTestMorphoPCVDeposit is Test { //// access controls function testEmergencyActionFailsNonGovernor() public { - MorphoPCVDeposit.Call[] memory calls = new MorphoPCVDeposit.Call[](1); + MorphoCompoundPCVDeposit.Call[] + memory calls = new MorphoCompoundPCVDeposit.Call[](1); calls[0].callData = abi.encodeWithSignature( "withdraw(address,uint256)", address(this), @@ -328,7 +331,7 @@ contract UnitTestMorphoPCVDeposit is Test { //// reentrancy function _reentrantSetup() private { - morphoDeposit = new MorphoPCVDeposit( + morphoDeposit = new MorphoCompoundPCVDeposit( address(core), address(maliciousMorpho), /// cToken is not used in mock morpho deposit address(token), @@ -340,7 +343,7 @@ contract UnitTestMorphoPCVDeposit is Test { vm.prank(addresses.governorAddress); core.grantLocker(address(morphoDeposit)); - maliciousMorpho.setMorphoPCVDeposit(address(morphoDeposit)); + maliciousMorpho.setMorphoCompoundPCVDeposit(address(morphoDeposit)); } function testReentrantAccrueFails() public { diff --git a/test/unit/system/System.t.sol b/test/unit/system/System.t.sol index f8b8e5f4e..e4a231e62 100644 --- a/test/unit/system/System.t.sol +++ b/test/unit/system/System.t.sol @@ -20,7 +20,7 @@ import {MockPCVDepositV2} from "@test/mock/MockPCVDepositV2.sol"; import {VoltSystemOracle} from "@voltprotocol/oracle/VoltSystemOracle.sol"; import {PegStabilityModule} from "@voltprotocol/peg/PegStabilityModule.sol"; import {IPCVDepositBalances} from "@voltprotocol/pcv/IPCVDepositBalances.sol"; -import {MorphoPCVDeposit} from "@voltprotocol/pcv/morpho/MorphoPCVDeposit.sol"; +import {MorphoCompoundPCVDeposit} from "@voltprotocol/pcv/morpho/MorphoCompoundPCVDeposit.sol"; import {TestAddresses as addresses} from "@test/unit/utils/TestAddresses.sol"; import {IGlobalReentrancyLock, GlobalReentrancyLock} from "@voltprotocol/core/GlobalReentrancyLock.sol"; import {IGlobalRateLimitedMinter, GlobalRateLimitedMinter} from "@voltprotocol/rate-limits/GlobalRateLimitedMinter.sol"; @@ -611,7 +611,7 @@ contract SystemUnitTest is Test { bytes4(keccak256("supply(address,address,uint256)")) ); - MorphoPCVDeposit deposit = new MorphoPCVDeposit( + MorphoCompoundPCVDeposit deposit = new MorphoCompoundPCVDeposit( address(core), address(mock), address(usdc), From d0726c0d949cf594e5d3cd2f9ac8a5cac882fff5 Mon Sep 17 00:00:00 2001 From: Elliot Date: Mon, 23 Jan 2023 17:59:25 -0800 Subject: [PATCH 26/74] Market Governance --- src/oracle/IPCVOracle.sol | 8 + src/oracle/PCVOracle.sol | 17 + src/pcv/IPCVMover.sol | 29 + src/pcv/IPCVRouter.sol | 36 -- src/pcv/PCVMover.sol | 140 +++++ src/pcv/PCVRouter.sol | 155 +---- src/vcon/MarketGovernance.sol | 569 ++++++++++++++++++ src/vcon/MarketGovernanceVenue.sol | 305 ---------- .../IntegrationTestPCVRouter.sol | 4 +- test/mock/MockPCVDepositV3.sol | 6 + test/mock/MockPCVRouter.sol | 39 ++ test/mock/MockPCVSwapper.sol | 32 +- test/unit/pcv/PCVRouter.t.sol | 6 +- test/unit/system/System.t.sol | 128 +++- test/unit/utils/Deviation.sol | 11 + test/unit/vcon/MarketGovernance.t.sol | 273 +++++++++ 16 files changed, 1219 insertions(+), 539 deletions(-) create mode 100644 src/pcv/IPCVMover.sol create mode 100644 src/pcv/PCVMover.sol create mode 100644 src/vcon/MarketGovernance.sol delete mode 100644 src/vcon/MarketGovernanceVenue.sol create mode 100644 test/mock/MockPCVRouter.sol create mode 100644 test/unit/vcon/MarketGovernance.t.sol diff --git a/src/oracle/IPCVOracle.sol b/src/oracle/IPCVOracle.sol index 5299c584b..817f83d14 100644 --- a/src/oracle/IPCVOracle.sol +++ b/src/oracle/IPCVOracle.sol @@ -34,6 +34,9 @@ interface IPCVOracle { /// @notice return all addresses listed as liquid venues function getVenues() external view returns (address[] memory); + /// @notice get total number of venues + function getNumVenues() external view returns (uint256); + /// @notice check if a venue is in the list of venues /// @param venue address to check /// @return boolean whether or not the venue is part of the venue list @@ -43,6 +46,11 @@ interface IPCVOracle { /// @dev this function is meant to be used offchain, as it is pretty gas expensive. function getTotalPcv() external view returns (uint256 totalPcv); + /// @notice get the total PCV balance by looping through the pcv deposits + /// @dev this function is meant to be used offchain, as it is pretty gas expensive. + /// this is an unsafe operation as it does not enforce the system is in an unlocked state + function getTotalPcvUnsafe() external view returns (uint256 totalPcv); + /// @notice returns decimal normalized version of a given venues USD balance function getVenueBalance(address venue) external view returns (uint256); diff --git a/src/oracle/PCVOracle.sol b/src/oracle/PCVOracle.sol index de0b8f196..536050baf 100644 --- a/src/oracle/PCVOracle.sol +++ b/src/oracle/PCVOracle.sol @@ -41,6 +41,11 @@ contract PCVOracle is IPCVOracle, CoreRefV2 { return venues.values(); } + /// @notice return all addresses listed as liquid venues + function getNumVenues() external view returns (uint256) { + return venues.length(); + } + /// @notice check if a venue is in the list of venues /// @param venue address to check /// @return boolean whether or not the venue is in the venue list @@ -56,6 +61,18 @@ contract PCVOracle is IPCVOracle, CoreRefV2 { "PCVOracle: cannot read while entered" ); + totalPcv = _getTotalPcv(); + } + + /// @notice get the total PCV balance by looping through the pcv deposits + /// @dev this function is meant to be used offchain, as it is pretty gas expensive. + /// this is an unsafe operation as it does not enforce the system is in an unlocked state + function getTotalPcvUnsafe() external view returns (uint256 totalPcv) { + totalPcv = _getTotalPcv(); + } + + /// @notice get the total PCV balance by looping through the pcv deposits + function _getTotalPcv() private view returns (uint256 totalPcv) { uint256 venueLength = venues.length(); /// there will never be more than 100 total venues diff --git a/src/pcv/IPCVMover.sol b/src/pcv/IPCVMover.sol new file mode 100644 index 000000000..0fcbfa0c7 --- /dev/null +++ b/src/pcv/IPCVMover.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.13; + +interface IPCVMover { + // ----------- Events ----------- + + event PCVMovement( + address indexed source, + address indexed destination, + uint256 amountSource, + uint256 amountDestination + ); + + event PCVSwapperAdded(address indexed swapper); + + event PCVSwapperRemoved(address indexed swapper); + + // ---------- Read-Only API ------------------ + + function isPCVSwapper(address pcvSwapper) external returns (bool); + + function getPCVSwappers() external returns (address[] memory); + + // ---------- PCVSwapper Management API ------ + + function addPCVSwappers(address[] calldata _pcvSwappers) external; + + function removePCVSwappers(address[] calldata _pcvSwappers) external; +} diff --git a/src/pcv/IPCVRouter.sol b/src/pcv/IPCVRouter.sol index d4712f9b9..c41774346 100644 --- a/src/pcv/IPCVRouter.sol +++ b/src/pcv/IPCVRouter.sol @@ -4,42 +4,6 @@ pragma solidity 0.8.13; /// @title a PCV Router interface /// @author Volt Protocol interface IPCVRouter { - // ----------- Events ----------- - - event PCVMovement( - address indexed source, - address indexed destination, - uint256 amountSource, - uint256 amountDestination - ); - - event PCVSwapperAdded(address indexed swapper); - - event PCVSwapperRemoved(address indexed swapper); - - // ---------- Read-Only API ------------------ - - function isPCVSwapper(address pcvSwapper) external returns (bool); - - function getPCVSwappers() external returns (address[] memory); - - // ---------- PCVSwapper Management API ------ - - function addPCVSwappers(address[] calldata _pcvSwappers) external; - - function removePCVSwappers(address[] calldata _pcvSwappers) external; - - // ----------- PCV_MOVER role API ----------- - - function movePCV( - address source, - address destination, - address swapper, - uint256 amount, - address sourceAsset, - address destinationAsset - ) external; - // ----------- PCV_CONTROLLER role api ----------- function movePCVUnchecked( diff --git a/src/pcv/PCVMover.sol b/src/pcv/PCVMover.sol new file mode 100644 index 000000000..ab78f0c0b --- /dev/null +++ b/src/pcv/PCVMover.sol @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.13; + +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + +import {console} from "@forge-std/console.sol"; +import {IPCVMover} from "@voltprotocol/pcv/IPCVMover.sol"; +import {CoreRefV2} from "@voltprotocol/refs/CoreRefV2.sol"; +import {IPCVOracle} from "@voltprotocol/oracle/IPCVOracle.sol"; +import {IPCVSwapper} from "@voltprotocol/pcv/IPCVSwapper.sol"; +import {IPCVDeposit} from "@voltprotocol/pcv/IPCVDeposit.sol"; + +abstract contract PCVMover is IPCVMover, CoreRefV2 { + using EnumerableSet for EnumerableSet.AddressSet; + + ///@notice set of whitelisted PCV swappers + EnumerableSet.AddressSet private pcvSwappers; + + // ---------- Read-Only API ------------------ + + /// @notice returns true if the the provided address is a valid swapper to use + /// @param pcvSwapper the pcvSwapper address to check if whitelisted + function isPCVSwapper( + address pcvSwapper + ) public view override returns (bool) { + return pcvSwappers.contains(pcvSwapper); + } + + /// @notice returns all whitelisted PCV swappers + function getPCVSwappers() + external + view + override + returns (address[] memory) + { + return pcvSwappers.values(); + } + + // ---------- PCVSwapper Management API ------ + + /// @notice Add multiple PCV Swappers to the whitelist + /// @param _pcvSwappers the addresses to whitelist, as calldata + function addPCVSwappers( + address[] calldata _pcvSwappers + ) external override onlyGovernor { + unchecked { + for (uint256 i = 0; i < _pcvSwappers.length; i++) { + require( + pcvSwappers.add(_pcvSwappers[i]), + "PCVRouter: Failed to add swapper" + ); + emit PCVSwapperAdded(_pcvSwappers[i]); + } + } + } + + /// @notice Remove multiple PCV Swappers from the whitelist + /// @param _pcvSwappers the addresses to remove from whitelist, as calldata + function removePCVSwappers( + address[] calldata _pcvSwappers + ) external override onlyGovernor { + unchecked { + for (uint256 i = 0; i < _pcvSwappers.length; i++) { + require( + pcvSwappers.remove(_pcvSwappers[i]), + "PCVRouter: Failed to remove swapper" + ); + emit PCVSwapperRemoved(_pcvSwappers[i]); + } + } + } + + /// ------------- Helper Methods ------------- + function _movePCV( + address source, + address destination, + address swapper, + uint256 amountSource, + address sourceAsset, + address destinationAsset + ) internal { + // Do transfer + uint256 amountDestination; + if (swapper != address(0)) { + IPCVDeposit(source).withdraw(swapper, amountSource); + amountDestination = IPCVSwapper(swapper).swap( + sourceAsset, + destinationAsset, + destination + ); + console.log("Successfully swapped: ", amountDestination); + } else { + IPCVDeposit(source).withdraw(destination, amountSource); + amountDestination = amountSource; + } + IPCVDeposit(destination).deposit(); + + // Emit event + emit PCVMovement(source, destination, amountSource, amountDestination); + } + + function _checkPCVMove( + address source, + address destination, + address swapper, + address sourceAsset, + address destinationAsset + ) internal view { + // Check both deposits are still valid for PCVOracle + IPCVOracle _pcvOracle = pcvOracle(); + require(_pcvOracle.isVenue(source), "PCVRouter: invalid source"); + require( + _pcvOracle.isVenue(destination), + "PCVRouter: invalid destination" + ); + + // Check underlying tokens + require( + IPCVDeposit(source).balanceReportedIn() == sourceAsset, + "PCVRouter: invalid source asset" + ); + require( + IPCVDeposit(destination).balanceReportedIn() == destinationAsset, + "PCVRouter: invalid destination asset" + ); + + if (sourceAsset != destinationAsset) { + require(swapper != address(0), "MarketGovernance: invalid swapper"); + } + + // Check swapper, if applicable + if (swapper != address(0)) { + require(isPCVSwapper(swapper), "PCVRouter: invalid swapper"); + require( + IPCVSwapper(swapper).canSwap(sourceAsset, destinationAsset), + "PCVRouter: unsupported swap" + ); + } + } +} diff --git a/src/pcv/PCVRouter.sol b/src/pcv/PCVRouter.sol index 64a32462c..9b0fec51f 100644 --- a/src/pcv/PCVRouter.sol +++ b/src/pcv/PCVRouter.sol @@ -1,8 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.13; -import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; - +import {PCVMover} from "@voltprotocol/pcv/PCVMover.sol"; import {CoreRefV2} from "@voltprotocol/refs/CoreRefV2.sol"; import {VoltRoles} from "@voltprotocol/core/VoltRoles.sol"; import {IPCVOracle} from "@voltprotocol/oracle/IPCVOracle.sol"; @@ -14,133 +13,11 @@ import {IPCVDeposit} from "@voltprotocol/pcv/IPCVDeposit.sol"; /// @notice A contract that allows PCV movements between deposits. /// @dev This contract requires the PCV_CONTROLLER role. /// @author eswak -contract PCVRouter is IPCVRouter, CoreRefV2 { - using EnumerableSet for EnumerableSet.AddressSet; - - ///@notice set of whitelisted PCV swappers - EnumerableSet.AddressSet private pcvSwappers; - +contract PCVRouter is IPCVRouter, CoreRefV2, PCVMover { constructor(address _core) CoreRefV2(_core) {} - // ---------- Read-Only API ------------------ - - /// @notice returns true if the the provided address is a valid swapper to use - /// @param pcvSwapper the pcvSwapper address to check if whitelisted - function isPCVSwapper( - address pcvSwapper - ) public view override returns (bool) { - return pcvSwappers.contains(pcvSwapper); - } - - /// @notice returns all whitelisted PCV swappers - function getPCVSwappers() - external - view - override - returns (address[] memory) - { - return pcvSwappers.values(); - } - - // ---------- PCVSwapper Management API ------ - - /// @notice Add multiple PCV Swappers to the whitelist - /// @param _pcvSwappers the addresses to whitelist, as calldata - function addPCVSwappers( - address[] calldata _pcvSwappers - ) external override onlyGovernor { - unchecked { - for (uint256 i = 0; i < _pcvSwappers.length; i++) { - require( - pcvSwappers.add(_pcvSwappers[i]), - "PCVRouter: Failed to add swapper" - ); - emit PCVSwapperAdded(_pcvSwappers[i]); - } - } - } - - /// @notice Remove multiple PCV Swappers from the whitelist - /// @param _pcvSwappers the addresses to remove from whitelist, as calldata - function removePCVSwappers( - address[] calldata _pcvSwappers - ) external override onlyGovernor { - unchecked { - for (uint256 i = 0; i < _pcvSwappers.length; i++) { - require( - pcvSwappers.remove(_pcvSwappers[i]), - "PCVRouter: Failed to remove swapper" - ); - emit PCVSwapperRemoved(_pcvSwappers[i]); - } - } - } - // ---------- PCV Movement API --------------- - /// @notice Move PCV by withdrawing it from a PCVDeposit and deposit it in - /// a destination PCVDeposit, eventually using a PCVSwapper in-between - /// for asset conversion. - /// This function requires a less trusted PCV_MOVER role, and performs checks - /// at runtime that the PCV Deposits are indeed added in the PCV Oracle, that - /// underlying tokens are correct, and that the PCVSwapper used (if any) has - /// previously been whitelisted through governance. - /// @param source the address of the pcv deposit contract to withdraw from - /// @param destination the address of the pcv deposit contract to deposit into - /// @param swapper the PCVSwapper to use for asset conversion, address(0) for no conversion. - /// @param amount the amount to withdraw and deposit - /// @param sourceAsset the token address of the source PCV Deposit - /// @param destinationAsset the token address of the destination PCV Deposit - function movePCV( - address source, - address destination, - address swapper, - uint256 amount, - address sourceAsset, - address destinationAsset - ) external whenNotPaused onlyVoltRole(VoltRoles.PCV_MOVER) globalLock(1) { - // Check both deposits are still valid for PCVOracle - IPCVOracle _pcvOracle = pcvOracle(); - require(_pcvOracle.isVenue(source), "PCVRouter: invalid source"); - require( - _pcvOracle.isVenue(destination), - "PCVRouter: invalid destination" - ); - - // Check underlying tokens - require( - IPCVDeposit(source).balanceReportedIn() == sourceAsset, - "PCVRouter: invalid source asset" - ); - require( - IPCVDeposit(destination).balanceReportedIn() == destinationAsset, - "PCVRouter: invalid destination asset" - ); - - if (sourceAsset != destinationAsset) { - require(swapper != address(0), "MarketGovernance: invalid swapper"); - } - - // Check swapper, if applicable - if (swapper != address(0)) { - require(isPCVSwapper(swapper), "PCVRouter: invalid swapper"); - require( - IPCVSwapper(swapper).canSwap(sourceAsset, destinationAsset), - "PCVRouter: unsupported swap" - ); - } - - // Do movement - _movePCV( - source, - destination, - swapper, - amount, - sourceAsset, - destinationAsset - ); - } - /// @notice Move PCV by withdrawing it from a PCVDeposit and deposit it in /// a destination PCVDeposit. /// This function requires the highly trusted PCV_CONTROLLER role, and expects @@ -197,32 +74,4 @@ contract PCVRouter is IPCVRouter, CoreRefV2 { destinationAsset ); } - - /// ------------- Helper Methods ------------- - function _movePCV( - address source, - address destination, - address swapper, - uint256 amountSource, - address sourceAsset, - address destinationAsset - ) internal { - // Do transfer - uint256 amountDestination; - if (swapper != address(0)) { - IPCVDeposit(source).withdraw(swapper, amountSource); - amountDestination = IPCVSwapper(swapper).swap( - sourceAsset, - destinationAsset, - destination - ); - } else { - IPCVDeposit(source).withdraw(destination, amountSource); - amountDestination = amountSource; - } - IPCVDeposit(destination).deposit(); - - // Emit event - emit PCVMovement(source, destination, amountSource, amountDestination); - } } diff --git a/src/vcon/MarketGovernance.sol b/src/vcon/MarketGovernance.sol new file mode 100644 index 000000000..6d28e4bda --- /dev/null +++ b/src/vcon/MarketGovernance.sol @@ -0,0 +1,569 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity =0.8.13; + +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; + +import {PCVMover} from "@voltprotocol/pcv/PCVMover.sol"; +import {Constants} from "@voltprotocol/Constants.sol"; +import {CoreRefV2} from "@voltprotocol/refs/CoreRefV2.sol"; +import {Deviation} from "@test/unit/utils/Deviation.sol"; +import {IPCVDepositV2} from "@voltprotocol/pcv/IPCVDepositV2.sol"; + +import {console} from "@forge-std/console.sol"; + +/// @notice this contract requires the PCV Controller and Locker role +/// Core formula for market governance rewards: +/// Profit Per VCON = Profit Per VCON + ∆Cumulative Profits (Dollars) * VCON:Dollar / VCON Staked +contract MarketGovernance is CoreRefV2, PCVMover { + using Deviation for *; + using SafeERC20 for *; + using SafeCast for *; + + /// @notice emitted when a route is added + event RouteAdded( + address indexed src, + address indexed dst, + address indexed swapper + ); + + /// @notice emitted when profit to vcon ratio is updated + event ProfitToVconRatioUpdated( + address indexed venue, + uint256 oldRatio, + uint256 newRatio + ); + + /// @notice total amount of VCON deposited across all venues + uint256 public totalSupply; + + /// @dev convention for all normal mappings is key (venue -> value) + + /// @notice amount of VCON paid per unit of revenue generated per venue + /// different venues may have different ratios to account for rewards + /// which will not be included in the V1 + mapping(address => uint256) public profitToVconRatio; + + /// TODO simplify this contract down to only track venue profit, + /// and do the conversion to VCON at the end when accruing rewards + /// alternatively, pack lastRecordedVconPricePerVenue, + /// lastRecordedProfit and totalVenueDepositedVcon into a single slot for gas optimization + + /// @notice last recorded profit index per venue + mapping(address => uint128) public lastRecordedProfit; + + /// @notice last recorded share price of a venue in VCON + /// starts off at 1e18 + mapping(address => uint128) public lastRecordedVconPricePerVenue; + + /// @notice total vcon deposited per venue + mapping(address => uint256) public totalVenueDepositedVcon; + + /// ---------- Per Venue User Profit Tracking ---------- + + /// @dev convention for all double nested address mappings is key (venue -> user) -> value + + /// @notice record of VCON index when user joined a given venue + mapping(address => mapping(address => uint256)) + public startingVenueVconProfit; + + /// @notice record how much VCON a user deposited in a given venue + mapping(address => mapping(address => uint256)) + public userVenueDepositedVcon; + + /// @param _core reference to core + constructor(address _core) CoreRefV2(_core) {} + + /// @notice permissionlessly initialize a venue + /// required to be able to utilize a given PCV Deposit in market governance + function initializeVenue(address venue) external globalLock(1) { + require(pcvOracle().isVenue(venue), "MarketGovernance: invalid venue"); + + uint256 startingLastRecordedProfit = lastRecordedProfit[venue]; + require( + startingLastRecordedProfit == 0, + "MarketGovernance: profit already recorded" + ); + require( + lastRecordedVconPricePerVenue[venue] == 0, + "MarketGovernance: venue already has share price" + ); + + IPCVDepositV2(venue).accrue(); + + uint256 endingLastRecordedProfit = IPCVDepositV2(venue) + .lastRecordedProfit(); + + lastRecordedProfit[venue] = endingLastRecordedProfit.toUint128(); + lastRecordedVconPricePerVenue[venue] = Constants + .ETH_GRANULARITY + .toUint128(); + } + + /// ---------- Permissionless User PCV Allocation Methods ---------- + + /// TODO update pcv oracle to cache total pcv so getAllPCV isn't needed to figure + /// out if weights are correct + /// @param amountVcon to stake on destination + /// @param amountPcv to move from source to destination + /// @param source address to pull funds from + /// @param destination address to accrue rewards to, and send funds to + /// @param swapper address that swaps asset types between src and dest if needed + function deposit( + uint256 amountVcon, + uint256 amountPcv, + address source, + address destination, + address swapper + ) external globalLock(1) { + _accrue(destination); /// update profitPerVCON in the destination so the user gets in at the current share price + uint256 vconRewards = _harvestRewards(msg.sender, destination); /// auto-compound rewards + /// check and an interaction with a trusted contract + vcon().safeTransferFrom(msg.sender, address(this), amountVcon); /// transfer VCON in + + uint256 totalVconDeposited = amountVcon + vconRewards; /// auto-compound rewards + + /// global updates + totalSupply += totalVconDeposited; + + /// user updates + userVenueDepositedVcon[destination][msg.sender] += totalVconDeposited; + + /// venue updates + totalVenueDepositedVcon[destination] += totalVconDeposited; + + /// ignore balance checks if only one user is allocating in the system + bool ignoreBalanceChecks = totalSupply == totalVconDeposited; + + _movePCVWithChecks( + source, + destination, + swapper, + amountPcv, + ignoreBalanceChecks + ); + } + + /// @notice deposit VCON without moving PCV + /// @param venue to stake VCON on + /// @param amountVcon amount of VCON to stake + function depositNoMove( + address venue, + uint256 amountVcon + ) external globalLock(1) { + /// update profitPerVCON in the destination so the user buys + /// in at the current share price + /// interaction, unfortunately it is necessary to do this before updating the user's balance + /// in order to ensure the user gets the current venue price when they stake + _accrue(venue); + + uint256 vconRewards = _harvestRewards(msg.sender, venue); /// auto-compound rewards + + uint256 totalVconDeposited = amountVcon + vconRewards; /// auto-compound rewards + + /// effects + + /// global updates + totalSupply += totalVconDeposited; + + /// user updates + userVenueDepositedVcon[venue][msg.sender] += totalVconDeposited; + + /// venue updates + totalVenueDepositedVcon[venue] += totalVconDeposited; + + /// interactions + + /// check and an interaction with a trusted contract + vcon().safeTransferFrom(msg.sender, address(this), amountVcon); /// transfer VCON in + } + + /// TODO add a function that decouples the movement of PCV from the + /// depositing so users can deposit without paying huge gas costs + + /// @notice unstake VCON and transfer corresponding VCON to another venue + /// @param amountVcon the amount of VCON staked to unstake + /// @param amountPcv the amount of PCV to move from source + /// @param source address to accrue rewards to, and pull funds from + /// @param destination address to send funds + /// @param vconRecipient address to receive the VCON + function withdraw( + uint256 amountVcon, + uint256 amountPcv, + address source, + address destination, + address swapper, + address vconRecipient + ) external globalLock(1) { + /// ---------- Check ---------- + + require(source != destination, "MarketGovernance: src and dest equal"); + + /// ---------- Effects ---------- + + _accrue(source); /// update profitPerVCON in the source so the user gets paid out at the current share price + { + uint256 vconRewards = _harvestRewards(msg.sender, source); /// pay msg.sender their rewards + + require( + userVenueDepositedVcon[source][msg.sender] + vconRewards >= + amountVcon, + "MarketGovernance: invalid vcon amount" + ); + + /// global updates + totalSupply -= amountVcon; + + /// user updates + /// balance = 80 + /// amount = 100 + /// rewards = 30 + /// amount deducted = 70 + /// _____________ + /// balance = 10 + userVenueDepositedVcon[source][msg.sender] -= + amountVcon - + vconRewards; + } + + /// venue updates + totalVenueDepositedVcon[destination] -= amountVcon; + + /// ---------- Interactions ---------- + + vcon().safeTransfer(vconRecipient, amountVcon); /// transfer VCON to recipient + + /// ignore balance checks if only one user is allocating in the system + bool ignoreBalanceChecks = totalSupply == + userVenueDepositedVcon[source][msg.sender]; + + _movePCVWithChecks( + source, + destination, + swapper, + amountPcv, + ignoreBalanceChecks + ); + } + + /// @notice rebalance PCV without staking or unstaking VCON + /// @param source address to pull funds from + /// @param destination recipient address for funds + /// @param swapper address that swaps denominations if necessary + /// @param amountPcv the amount of PCV to move from source + function rebalance( + address source, + address destination, + address swapper, + uint256 amountPcv + ) external globalLock(1) { + _movePCVWithChecks(source, destination, swapper, amountPcv, false); + } + + struct Rebalance { + address source; + address destination; + address swapper; + uint256 amountPcv; + } + + /// @notice rebalance PCV without staking or unstaking VCON + /// each individual action must make the system more balanced + /// as a whole, otherwise it will revert + /// @param movements information on all pcv movements + /// including sources, destinations, amounts and swappers + function rebalanceBulk( + Rebalance[] calldata movements + ) external globalLock(1) { + unchecked { + for (uint256 i = 0; i < movements.length; i++) { + address source = movements[i].source; + address destination = movements[i].destination; + address swapper = movements[i].swapper; + uint256 amountPcv = movements[i].amountPcv; + + _movePCVWithChecks( + source, + destination, + swapper, + amountPcv, + false + ); + } + } + } + + /// TODO add slashing logic to burn VCON from participants that are in a venue that took a loss + + /// ------------- View Only Methods ------------- + + /// @notice returns positive value if over allocated + /// returns negative value if under allocated + /// if no venue balance and deposited vcon, return positive + /// if venue balance and no deposited vcon, return negative + /// + /// algorithm for determining how balanced a venue is: + /// + /// 1. get ratio of how much vcon is deposited into the venue out of the total supply + /// a = venue deposited vcon / total vcon staked + /// + /// 2. get expected amount of pcv in the venue based on the vcon staked in that venue and the total pcv + /// b = a * total pcv + /// + /// 3. find the delta in basis points between the expected amount of pcv in the venue vs the actual amount in the venue + /// d = (a - b) / a + /// + /// @param venue to query + /// @param totalPcv to measure venue against + function getVenueBalance( + address venue, + uint256 totalPcv + ) public view returns (int256) { + uint256 venueDepositedVcon = totalVenueDepositedVcon[venue]; + uint256 venueBalance = pcvOracle().getVenueBalance(venue); + uint256 cachedTotalSupply = totalSupply; + + /// 0 checks as any 0 denominator will cause a revert + if ( + (venueDepositedVcon == 0 && venueBalance == 0) || + cachedTotalSupply == 0 || + totalPcv == 0 + ) { + return 0; /// perfectly balanced at 0 PCV or 0 VCON staked + } + + /// Step 1. + /// find out actual ratio of VCON in a given venue based on total VCON staked + uint256 venueDepositedVconRatio = (venueDepositedVcon * + Constants.ETH_GRANULARITY) / cachedTotalSupply; + + /// perfectly balanced + if (venueDepositedVconRatio == 0 && venueBalance == 0) { + return 0; + } + + if (venueDepositedVconRatio == 0) { + /// add this 0 check because deviation divides by a and would cause a revert + /// replicate step 3 if no VCON is deposited by comparing pcv to venue balance + return + Deviation.calculateDeviationEthGranularity( + totalPcv.toInt256(), + venueBalance.toInt256() + ); + } + + /// Step 2. + /// get expected pcv amount + uint256 expectedPcvAmount = (venueDepositedVconRatio * totalPcv) / + Constants.ETH_GRANULARITY; + + /// perfectly balanced = (expectedPcvAmount - venueBalance) * 1e18 / expectedPcvAmount = 0 + + /// Step 3. + /// if venue deposited VCON, return the ratio between expected pcv vs actual pcv + return + -Deviation.calculateDeviationEthGranularity( + expectedPcvAmount.toInt256(), + venueBalance.toInt256() + ); + } + + struct PCVDepositInfo { + address deposit; + uint256 amount; + } + + /// @notice return what the perfectly balanced system would look like + function getExpectedPCVAmounts() + public + view + returns (PCVDepositInfo[] memory deposits) + { + address[] memory pcvDeposits = pcvOracle().getVenues(); + uint256 totalVenues = pcvDeposits.length; + uint256 totalPcv = pcvOracle().getTotalPcv(); + uint256 cachedTotalSupply = totalSupply; /// Save repeated warm SLOADs + + deposits = new PCVDepositInfo[](totalVenues); + + unchecked { + for (uint256 i = 0; i < totalVenues; i++) { + address venue = pcvDeposits[i]; + uint256 venueDepositedVcon = totalVenueDepositedVcon[venue]; + deposits[i].deposit = venue; + + if (venueDepositedVcon == 0) { + deposits[i].amount = 0; + continue; + } else { + uint256 expectedPcvAmount = (venueDepositedVcon * + totalPcv) / cachedTotalSupply; + + deposits[i].amount = expectedPcvAmount; + } + } + } + } + + /// ------------- Helper Methods ------------- + + /// @param source address to pull funds from + /// @param destination recipient address for funds + /// @param swapper address to swap tokens with + /// @param amountPcv the amount of PCV to move from source + /// @param ignoreBalanceChecks whether or not to ignore balance checks + function _movePCVWithChecks( + address source, + address destination, + address swapper, + uint256 amountPcv, + bool ignoreBalanceChecks + ) private { + address sourceAsset = IPCVDepositV2(source).balanceReportedIn(); + address destinationAsset = IPCVDepositV2(destination) + .balanceReportedIn(); + + /// read unsafe because we are at lock level 1 + uint256 totalPcv = pcvOracle().getTotalPcvUnsafe(); + + int256 sourceVenueBalance; + int256 destinationVenueBalance; + + if (!ignoreBalanceChecks) { + /// record how balanced the system is before the PCV movement + sourceVenueBalance = getVenueBalance(source, totalPcv); /// 50% + destinationVenueBalance = getVenueBalance(destination, totalPcv); /// -30% + } + + /// validate pcv movement + /// check underlying assets match up and if not that swapper is provided and valid + _checkPCVMove( + source, + destination, + swapper, + sourceAsset, + destinationAsset + ); + + /// optimistically transfer funds to the specified pcv deposit + _movePCV( + source, + destination, + swapper, + amountPcv, + sourceAsset, + destinationAsset + ); + + if (!ignoreBalanceChecks) { + /// TODO simplify this by passing delta of starting balance instead of calling balance again on each pcv deposit + /// record how balanced the system is before the PCV movement + int256 sourceVenueBalanceAfter = getVenueBalance(source, totalPcv); + int256 destinationVenueBalanceAfter = getVenueBalance( + destination, + totalPcv + ); + + /// source and dest venue balance measures the distance from being perfectly balanced + + /// validate source venue balance became more balanced + _checkBalance( + sourceVenueBalance, + sourceVenueBalanceAfter, + "MarketGovernance: src more imbalanced" + ); + + /// validate destination venue balance became more balanced + _checkBalance( + destinationVenueBalance, + destinationVenueBalanceAfter, + "MarketGovernance: dest more imbalanced" + ); + } + } + + /// @notice helper function to validate balance moved in the right direction after a pcv movement + function _checkBalance( + int256 balanceBefore, + int256 balanceAfter, + string memory reason + ) private pure { + require( + balanceBefore < 0 /// if balance is under weight relative to vcon staked, ensure it doesn't go over balance + ? balanceAfter > balanceBefore && balanceAfter <= 0 /// if balance is over weight relative to vcon staked, ensure it doesn't go under balance + : balanceAfter < balanceBefore && balanceAfter >= 0, + reason + ); + } + + /// @notice returns profit in VCON a user has accrued + /// does not update how much VCON a user has staked to save on gas + /// that updating happens in the calling function + function _harvestRewards( + address user, + address venue + ) private returns (uint256) { + uint256 vconStartIndex = startingVenueVconProfit[venue][user]; + uint256 vconCurrentIndex = lastRecordedVconPricePerVenue[venue]; + + /// do not pay out if user has not entered the market + if (vconStartIndex == 0) { + /// set user starting profit to current venue profit index + startingVenueVconProfit[venue][user] = vconCurrentIndex; + return 0; /// no profits + } + + uint256 vconRewards = (vconCurrentIndex - vconStartIndex) * + userVenueDepositedVcon[venue][user]; + startingVenueVconProfit[venue][user] = vconCurrentIndex; + + return vconRewards; + } + + function _accrue(address venue) private { + uint256 startingLastRecordedProfit = lastRecordedProfit[venue]; + + IPCVDepositV2(venue).accrue(); + + uint256 endingLastRecordedProfit = IPCVDepositV2(venue) + .lastRecordedProfit(); + int256 deltaProfit = endingLastRecordedProfit.toInt256() - + startingLastRecordedProfit.toInt256(); /// also could be a loss + + /// amount of VCON that each VCON deposit receives for being in this venue + /// if there is no supply, there is no delta to apply + if (totalSupply != 0) { + int256 venueProfitToVconRatio = profitToVconRatio[venue].toInt256(); + + int256 profitPerVconDelta = (deltaProfit * venueProfitToVconRatio) / + totalSupply.toInt256(); + + uint256 lastVconSharePrice = lastRecordedVconPricePerVenue[venue]; + require( + lastVconSharePrice >= Constants.ETH_GRANULARITY, + "MarketGovernance: venue not initialized" + ); + + lastRecordedVconPricePerVenue[venue] = (lastVconSharePrice + .toInt256() + profitPerVconDelta).toUint256().toUint128(); + } + } + + /// TODO add events + + /// ---------- Governor-Only Permissioned API ---------- + + function setProfitToVconRatio( + address venue, + uint256 newProfitToVconRatio + ) external onlyGovernor { + uint256 oldProfitToVconRatio = profitToVconRatio[venue]; + profitToVconRatio[venue] = newProfitToVconRatio; + + emit ProfitToVconRatioUpdated( + venue, + oldProfitToVconRatio, + newProfitToVconRatio + ); + } +} diff --git a/src/vcon/MarketGovernanceVenue.sol b/src/vcon/MarketGovernanceVenue.sol deleted file mode 100644 index 4b458a58a..000000000 --- a/src/vcon/MarketGovernanceVenue.sol +++ /dev/null @@ -1,305 +0,0 @@ -pragma solidity =0.8.13; - -import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; - -import {Constants} from "@voltprotocol/Constants.sol"; -import {PCVRouter} from "@voltprotocol/pcv/PCVRouter.sol"; -import {CoreRefV2} from "@voltprotocol/refs/CoreRefV2.sol"; -import {Deviation} from "@test/unit/utils/Deviation.sol"; -import {IPCVDepositV2} from "@voltprotocol/pcv/IPCVDepositV2.sol"; - -contract MarketGovernanceVenue is CoreRefV2 { - using Deviation for *; - using SafeERC20 for *; - using SafeCast for *; - - /// @notice reference to PCV Router - address public pcvRouter; - - /// @notice amount of VCON paid per unit of revenue generated - uint256 public profitToVconRatio; - - /// @notice total amount of VCON deposited across all venues - uint256 public totalSupply; - - /// TODO simplify this contract down to only track venue profit, - /// and do the conversion to VCON at the end when accruing rewards - - /// @notice last recorded profit index per venue - mapping(address => uint128) public lastRecordedProfit; - - /// @notice last recorded share price of a venue in VCON - /// starts off at 1e18 - mapping(address => uint128) public lastRecordedVconPricePerVenue; - - /// @notice total vcon deposited per venue - mapping(address => uint256) public totalVenueDepositedVcon; - - /// ---------- Per Venue User Profit Tracking ---------- - - /// @dev convention for all double nested address mappings is key (venue -> user) -> value - - /// @notice record of VCON index when user joined a given venue - mapping(address => mapping(address => uint256)) - public startingVenueVconProfit; - - /// @notice record how much VCON a user deposited in a given venue - mapping(address => mapping(address => uint256)) - public userVenueDepositedVcon; - - /// ---------- Per Venue User Profit Tracking ---------- - - /// @notice approved routes to swap different tokens and their corresponding swapper address - /// first address is token from, second address is tokenTo, third address is corresponding swapper - /// the starting approved routes will be DAI -> USDC and USDC -> DAI through the Maker PCV Swapper - mapping(address => mapping(address => address)) public approvedRoutes; - - /// @param _core reference to core - /// @param _pcvRouter reference to the PCV Router - /// @param _profitToVconRatio ratio of VCON paid out per dollar in revenue - constructor( - address _core, - address _pcvRouter, - uint256 _profitToVconRatio - ) CoreRefV2(_core) { - pcvRouter = _pcvRouter; - profitToVconRatio = _profitToVconRatio; - } - - /// TODO update pcv oracle to cache total pcv so getAllPCV isn't needed to figure - /// out if weights are correct - /// TODO change movePCV to use a different global reentrancy lock state, or require system be lock level 1 to use that function - /// @param source address to pull funds from - /// @param destination address to accrue rewards to, and send funds to - function deposit( - uint256 amountVcon, - uint256 amountPcv, - address source, - address destination - ) external globalLock(1) { - _accrue(destination); /// update profitPerVCON in the destination so the user gets in at the current share price - uint256 vconRewards = _harvestRewards(msg.sender, destination); /// auto-compound rewards - /// check and an interaction with a trusted contract - vcon().safeTransferFrom(msg.sender, address(this), amountVcon); /// transfer VCON in - - uint256 totalVconDeposited = amountVcon + vconRewards; /// auto-compound rewards - - /// global updates - totalSupply += totalVconDeposited; - - /// user updates - userVenueDepositedVcon[destination][msg.sender] += totalVconDeposited; - - /// venue updates - totalVenueDepositedVcon[destination] += totalVconDeposited; - - _movePcv(source, destination, amountPcv); - } - - /// @notice unstake VCON and transfer corresponding VCON to another venue - /// @param amountVcon the amount of VCON staked to unstake - /// @param source address to accrue rewards to, and pull funds from - function withdraw( - uint256 amountVcon, - uint256 amountPcv, - address source, - address destination, - address vconRecipient - ) external globalLock(1) { - require(source != destination, "MarketGovernance: src and dest equal"); - - _accrue(source); /// update profitPerVCON in the source so the user gets paid out at the current share price - { - uint256 vconRewards = _harvestRewards(msg.sender, source); /// pay msg.sender their rewards - - require( - userVenueDepositedVcon[source][msg.sender] + vconRewards >= - amountVcon, - "MarketGovernance: invalid vcon amount" - ); - - /// check and an interaction with a trusted contract - - vcon().safeTransfer(vconRecipient, amountVcon); /// transfer VCON to recipient - - /// global updates - totalSupply -= amountVcon; - - /// user updates - /// balance = 80 - /// amount = 100 - /// rewards = 30 - /// amount deducted = 70 - /// _____________ - /// balance = 10 - userVenueDepositedVcon[source][msg.sender] -= - amountVcon - - vconRewards; - } - - /// venue updates - totalVenueDepositedVcon[destination] -= amountVcon; - - _movePcv(source, destination, amountPcv); - } - - /// @notice rebalance PCV without staking or unstaking VCON - function rebalance( - address source, - address destination, - uint256 amountPcv - ) external globalLock(1) { - _movePcv(source, destination, amountPcv); - } - - function _movePcv( - address source, - address destination, - uint256 amountPcv - ) private { - address sourceAsset = IPCVDepositV2(source).balanceReportedIn(); - address destinationAsset = IPCVDepositV2(destination) - .balanceReportedIn(); - address swapper = approvedRoutes[sourceAsset][destinationAsset]; - - /// TODO fix this method on the oracle because it doesn't allow the read while in lock mode - uint256 totalPcv = pcvOracle().getTotalPcv(); - - /// record how balanced the system is before the PCV movement - int256 sourceVenueBalance = getVenueBalance(source, totalPcv); - int256 destinationVenueBalance = getVenueBalance(destination, totalPcv); - - /// optimistically transfer funds to the specified pcv deposit - /// swapper validity not checked in this contract as the PCV Router will check this - PCVRouter(pcvRouter).movePCV( - source, - destination, - swapper, - amountPcv, - sourceAsset, - destinationAsset - ); - - /// TODO simplify this by passing delta of starting balance instead of calling balance again on each pcv deposit - /// record how balanced the system is before the PCV movement - int256 sourceVenueBalanceAfter = getVenueBalance(source, totalPcv); - int256 destinationVenueBalanceAfter = getVenueBalance( - destination, - totalPcv - ); - - /// source and dest venue balance measures the distance from being perfectly balanced - /// validate source venue balance became more balanced - require( - sourceVenueBalance < 0 /// if balance is under weight relative to vcon staked, ensure it doesn't go over balance - ? sourceVenueBalanceAfter > sourceVenueBalance && - sourceVenueBalanceAfter <= 0 /// if balance is over weight relative to vcon staked, ensure it doesn't go under balance - : sourceVenueBalanceAfter < sourceVenueBalance && - sourceVenueBalanceAfter >= 0, - "MarketGovernance: src more imbalanced" - ); - - /// validate destination venue balance became more balanced - require( - destinationVenueBalance < 0 /// if balance is under weight relative to vcon staked, ensure it doesn't go over balance - ? destinationVenueBalanceAfter > destinationVenueBalance && - destinationVenueBalanceAfter <= 0 /// if balance is over weight relative to vcon staked, ensure it doesn't go under balance - : destinationVenueBalanceAfter > destinationVenueBalance && - destinationVenueBalanceAfter >= 0, - "MarketGovernance: src more imbalanced" - ); - } - - /// @notice returns profit in VCON a user has accrued - /// does not update how much VCON a user has staked to save on gas - /// that updating happens in the calling function - function _harvestRewards( - address user, - address venue - ) private returns (uint256) { - uint256 vconStartIndex = startingVenueVconProfit[venue][user]; - uint256 vconCurrentIndex = lastRecordedVconPricePerVenue[venue]; - - /// do not pay out if user has not entered the market - if (vconStartIndex == 0) { - /// set user starting profit to current venue profit index - startingVenueVconProfit[venue][user] = vconCurrentIndex; - return 0; /// no profits - } - - uint256 vconRewards = (vconCurrentIndex - vconStartIndex) * - userVenueDepositedVcon[venue][user]; - startingVenueVconProfit[venue][user] = vconCurrentIndex; - - return vconRewards; - } - - function _accrue(address venue) private { - uint256 startingLastRecordedProfit = lastRecordedProfit[venue]; - - IPCVDepositV2(venue).accrue(); - - uint256 endingLastRecordedProfit = IPCVDepositV2(venue) - .lastRecordedProfit(); - int256 deltaProfit = endingLastRecordedProfit.toInt256() - - startingLastRecordedProfit.toInt256(); /// also could be a loss - - /// amount of VCON that each VCON deposit receives for being in this venue - int256 profitPerVconDelta = (deltaProfit * - profitToVconRatio.toInt256()) / totalSupply.toInt256(); - - lastRecordedVconPricePerVenue[venue] = (lastRecordedVconPricePerVenue[ - venue - ].toInt256() + profitPerVconDelta).toUint256().toUint128(); - } - - /// @notice returns positive value if over allocated - /// returns negative value if under allocated - function getVenueBalance( - address venue, - uint256 totalPcv - ) public view returns (int256) { - uint256 venueDepositedVcon = totalVenueDepositedVcon[venue]; - - uint256 venueBalance = pcvOracle().getVenueBalance(venue); - - if (venueDepositedVcon == 0 && venueBalance == 0) { - return 0; /// perfectly balanced at 0 PCV - } - - /// if no venue balance and deposited vcon, return positive - /// if venue balance and no deposited vcon, return negative - - /// find out the actual ratio of PCV in a given venue based on PCV - uint256 venueRatio = (venueBalance * Constants.ETH_GRANULARITY) / - totalPcv; - - /// find out expected ratio of PCV in a given venue based on VCON staked - uint256 venueDepositedVconRatio = (venueDepositedVcon * - Constants.ETH_GRANULARITY) / totalSupply; - - /// if no venue deposited vcon, we would divide by 0, and revert, so reverse order and multiply by -1 - /// this means there is too much PCV for the amount of VCON in the venue - if (venueDepositedVconRatio == 0) { - return - -1 * - Deviation.calculateDeviationBasisPoints( - venueRatio.toInt256(), - venueDepositedVconRatio.toInt256() - ); - } - - /// if venue deposited VCON, return the regular ratio between vcon deposited and pcv deposited - return - Deviation.calculateDeviationBasisPoints( - venueDepositedVconRatio.toInt256(), - venueRatio.toInt256() - ); - } - - /// TODO add governance API's to change the pcv router - /// profitToVconRatio - /// approved routes - /// TODO add events -} diff --git a/test/integration/post-proposal-checks/IntegrationTestPCVRouter.sol b/test/integration/post-proposal-checks/IntegrationTestPCVRouter.sol index 4cefd02eb..0a9a67afb 100644 --- a/test/integration/post-proposal-checks/IntegrationTestPCVRouter.sol +++ b/test/integration/post-proposal-checks/IntegrationTestPCVRouter.sol @@ -28,7 +28,7 @@ contract IntegrationTestPCVRouter is PostProposalCheck { // Swap DAI to USDC vm.startPrank(pcvMover); - pcvRouter.movePCV( + pcvRouter.movePCVUnchecked( address(daiDeposit), // source address(usdcDeposit), // destination addresses.mainnet("PCV_SWAPPER_MAKER"), // swapper @@ -53,7 +53,7 @@ contract IntegrationTestPCVRouter is PostProposalCheck { // Swap USDC to DAI (half of previous amount) vm.startPrank(pcvMover); // has PCV_MOVER role - pcvRouter.movePCV( + pcvRouter.movePCVUnchecked( address(usdcDeposit), // source address(daiDeposit), // destination addresses.mainnet("PCV_SWAPPER_MAKER"), // swapper diff --git a/test/mock/MockPCVDepositV3.sol b/test/mock/MockPCVDepositV3.sol index bec9420cf..73235ea54 100644 --- a/test/mock/MockPCVDepositV3.sol +++ b/test/mock/MockPCVDepositV3.sol @@ -13,6 +13,8 @@ contract MockPCVDepositV3 is IPCVDeposit, CoreRefV2 { uint256 private resistantBalance; uint256 private resistantProtocolOwnedVolt; + uint256 public lastRecordedProfit; + constructor(address _core, address _token) CoreRefV2(_core) { balanceReportedIn = _token; } @@ -27,6 +29,10 @@ contract MockPCVDepositV3 is IPCVDeposit, CoreRefV2 { resistantProtocolOwnedVolt = _resistantProtocolOwnedVolt; } + function setLastRecordedProfit(uint256 _lastRecordedProfit) public { + lastRecordedProfit = _lastRecordedProfit; + } + function setCheckPCVController(bool value) public { checkPCVController = value; } diff --git a/test/mock/MockPCVRouter.sol b/test/mock/MockPCVRouter.sol new file mode 100644 index 000000000..171659c49 --- /dev/null +++ b/test/mock/MockPCVRouter.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.13; + +import {PCVRouter} from "@voltprotocol/pcv/PCVRouter.sol"; +import {VoltRoles} from "@voltprotocol/core/VoltRoles.sol"; + +contract MockPCVRouter is PCVRouter { + constructor(address _core) PCVRouter(_core) {} + + function movePCV( + address source, + address destination, + address swapper, + uint256 amount, + address sourceAsset, + address destinationAsset + ) external onlyVoltRole(VoltRoles.PCV_MOVER) whenNotPaused globalLock(1) { + /// validate pcv movement + /// check underlying assets match up and if not that swapper is provided and valid + _checkPCVMove( + source, + destination, + swapper, + sourceAsset, + destinationAsset + ); + + /// optimistically transfer funds to the specified pcv deposit + /// swapper validity not checked in this contract as the PCV Router will check this + _movePCV( + source, + destination, + swapper, + amount, + sourceAsset, + destinationAsset + ); + } +} diff --git a/test/mock/MockPCVSwapper.sol b/test/mock/MockPCVSwapper.sol index 63abdca34..5a6747763 100644 --- a/test/mock/MockPCVSwapper.sol +++ b/test/mock/MockPCVSwapper.sol @@ -1,5 +1,7 @@ pragma solidity 0.8.13; +import {console} from "@forge-std/console.sol"; + import {MockERC20} from "@test/mock/MockERC20.sol"; import {IPCVSwapper} from "@voltprotocol/pcv/IPCVSwapper.sol"; @@ -20,8 +22,10 @@ contract MockPCVSwapper is IPCVSwapper { function canSwap( address assetIn, address assetOut - ) external view returns (bool) { - return address(tokenIn) == assetIn && address(tokenOut) == assetOut; + ) public view returns (bool) { + return + (address(tokenIn) == assetIn && address(tokenOut) == assetOut) || + (address(tokenIn) == assetOut && address(tokenOut) == assetIn); } function swap( @@ -29,16 +33,20 @@ contract MockPCVSwapper is IPCVSwapper { address assetOut, address destination ) external returns (uint256) { - require(assetIn == address(tokenIn), "MockPCVSwapper: invalid assetIn"); - require( - assetOut == address(tokenOut), - "MockPCVSwapper: invalid assetOut" - ); - - uint256 amountIn = tokenIn.balanceOf(address(this)); - uint256 amountOut = (amountIn * exchangeRate) / 1e18; - tokenIn.mockBurn(address(this), amountIn); - tokenOut.mint(destination, amountOut); + require(canSwap(assetIn, assetOut), "MockPCVSwapper: invalid swap"); + + uint256 amountIn = MockERC20(assetIn).balanceOf(address(this)); + + /// if regular flow, do regular rate, otherwise reverse rate + uint256 amountOut = address(tokenIn) == assetIn + ? (amountIn * exchangeRate) / 1e18 + : ((amountIn * 1e18) / exchangeRate); + + console.log("amountIn: ", amountIn); + console.log("amountOut: ", amountOut); + + MockERC20(assetIn).mockBurn(address(this), amountIn); + MockERC20(assetOut).mint(destination, amountOut); emit Swap(assetIn, assetOut, destination, amountIn, amountOut); diff --git a/test/unit/pcv/PCVRouter.t.sol b/test/unit/pcv/PCVRouter.t.sol index fe36a72e9..b74ebc745 100644 --- a/test/unit/pcv/PCVRouter.t.sol +++ b/test/unit/pcv/PCVRouter.t.sol @@ -4,7 +4,6 @@ pragma solidity 0.8.13; import {Vm} from "@forge-std/Vm.sol"; import {Test} from "@forge-std/Test.sol"; import {CoreV2} from "@voltprotocol/core/CoreV2.sol"; -import {PCVRouter} from "@voltprotocol/pcv/PCVRouter.sol"; import {PCVOracle} from "@voltprotocol/oracle/PCVOracle.sol"; import {MockERC20} from "@test/mock/MockERC20.sol"; import {VoltRoles} from "@voltprotocol/core/VoltRoles.sol"; @@ -12,6 +11,7 @@ import {getCoreV2} from "@test/unit/utils/Fixtures.sol"; import {IPCVOracle} from "@voltprotocol/oracle/IPCVOracle.sol"; import {MockOracle} from "@test/mock/MockOracle.sol"; import {SystemEntry} from "@voltprotocol/entry/SystemEntry.sol"; +import {MockPCVRouter} from "@test/mock/MockPCVRouter.sol"; import {MockPCVSwapper} from "@test/mock/MockPCVSwapper.sol"; import {MockPCVDepositV3} from "@test/mock/MockPCVDepositV3.sol"; import {TestAddresses as addresses} from "@test/unit/utils/TestAddresses.sol"; @@ -25,7 +25,7 @@ contract PCVRouterUnitTest is Test { PCVOracle private pcvOracle; // reference to the volt pcv router - PCVRouter private pcvRouter; + MockPCVRouter private pcvRouter; // global reentrancy lock IGlobalReentrancyLock private lock; @@ -72,7 +72,7 @@ contract PCVRouterUnitTest is Test { // volt system core = CoreV2(address(getCoreV2())); pcvOracle = new PCVOracle(address(core)); - pcvRouter = new PCVRouter(address(core)); + pcvRouter = new MockPCVRouter(address(core)); entry = new SystemEntry(address(core)); lock = IGlobalReentrancyLock( address(new GlobalReentrancyLock(address(core))) diff --git a/test/unit/system/System.t.sol b/test/unit/system/System.t.sol index ab4068461..9f9212345 100644 --- a/test/unit/system/System.t.sol +++ b/test/unit/system/System.t.sol @@ -5,21 +5,26 @@ import {TimelockController} from "@openzeppelin/contracts/governance/TimelockCon import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {console} from "@forge-std/console.sol"; + import {Test} from "@forge-std/Test.sol"; import {ICoreV2} from "@voltprotocol/core/ICoreV2.sol"; import {Deviation} from "@test/unit/utils/Deviation.sol"; import {VoltRoles} from "@voltprotocol/core/VoltRoles.sol"; import {MockERC20} from "@test/mock/MockERC20.sol"; +import {PCVRouter} from "@voltprotocol/pcv/PCVRouter.sol"; +import {PCVOracle} from "@voltprotocol/oracle/PCVOracle.sol"; import {PCVDeposit} from "@voltprotocol/pcv/PCVDeposit.sol"; import {SystemEntry} from "@voltprotocol/entry/SystemEntry.sol"; import {PCVGuardian} from "@voltprotocol/pcv/PCVGuardian.sol"; import {MockCoreRefV2} from "@test/mock/MockCoreRefV2.sol"; import {ERC20Allocator} from "@voltprotocol/pcv/ERC20Allocator.sol"; import {GenericCallMock} from "@test/mock/GenericCallMock.sol"; -import {MockPCVDepositV2} from "@test/mock/MockPCVDepositV2.sol"; +import {MockPCVDepositV3} from "@test/mock/MockPCVDepositV3.sol"; import {VoltSystemOracle} from "@voltprotocol/oracle/VoltSystemOracle.sol"; import {PegStabilityModule} from "@voltprotocol/peg/PegStabilityModule.sol"; import {IPCVDepositBalances} from "@voltprotocol/pcv/IPCVDepositBalances.sol"; +import {ConstantPriceOracle} from "@voltprotocol/oracle/ConstantPriceOracle.sol"; import {MorphoCompoundPCVDeposit} from "@voltprotocol/pcv/morpho/MorphoCompoundPCVDeposit.sol"; import {TestAddresses as addresses} from "@test/unit/utils/TestAddresses.sol"; import {IGlobalReentrancyLock, GlobalReentrancyLock} from "@voltprotocol/core/GlobalReentrancyLock.sol"; @@ -63,24 +68,29 @@ contract SystemUnitTest is Test { using SafeCast for *; VoltAddresses public guardianAddresses = getVoltAddresses(); - ICoreV2 private core; - SystemEntry private entry; - PegStabilityModule private daipsm; - PegStabilityModule private usdcpsm; - MockPCVDepositV2 private pcvDepositDai; - MockPCVDepositV2 private pcvDepositUsdc; - PCVGuardian private pcvGuardian; - ERC20Allocator private allocator; - VoltSystemOracle private oracle; - TimelockController private timelockController; - GlobalRateLimitedMinter private grlm; - IGlobalReentrancyLock private lock; - - address private voltAddress; - address private coreAddress; - IERC20Mintable private usdc; - IERC20Mintable private dai; - IERC20Mintable private volt; + ICoreV2 public core; + SystemEntry public entry; + PegStabilityModule public daipsm; + PegStabilityModule public usdcpsm; + MockPCVDepositV3 public pcvDepositDai; + MockPCVDepositV3 public pcvDepositUsdc; + PCVGuardian public pcvGuardian; + ERC20Allocator public allocator; + VoltSystemOracle public oracle; + TimelockController public timelockController; + GlobalRateLimitedMinter public grlm; + IGlobalReentrancyLock public lock; + PCVRouter public pcvRouter; + PCVOracle public pcvOracle; + ConstantPriceOracle public daiConstantOracle; + ConstantPriceOracle public usdcConstantOracle; + + address public voltAddress; + address public coreAddress; + IERC20Mintable public usdc; + IERC20Mintable public dai; + IERC20Mintable public volt; + IERC20Mintable public vcon; uint256 public constant timelockDelay = 600; uint248 public constant usdcTargetBalance = 100_000e6; @@ -119,11 +129,12 @@ contract SystemUnitTest is Test { uint112 public constant monthlyChangeRate = .01e18; /// 100 basis points uint32 public constant startTime = 1_000; - function setUp() public { + function setUp() public virtual { vm.warp(startTime); /// warp past 0 core = getCoreV2(); entry = new SystemEntry(address(core)); volt = IERC20Mintable(address(core.volt())); + vcon = IERC20Mintable(address(core.vcon())); voltAddress = address(volt); coreAddress = address(core); lock = IGlobalReentrancyLock( @@ -166,13 +177,14 @@ contract SystemUnitTest is Test { voltCeilingPriceDai ); - pcvDepositDai = new MockPCVDepositV2(coreAddress, address(dai), 100, 0); - pcvDepositUsdc = new MockPCVDepositV2( - coreAddress, - address(usdc), - 100, - 0 - ); + pcvDepositDai = new MockPCVDepositV3(coreAddress, address(dai)); + pcvDepositUsdc = new MockPCVDepositV3(coreAddress, address(usdc)); + + pcvRouter = new PCVRouter(coreAddress); + + pcvOracle = new PCVOracle(coreAddress); + daiConstantOracle = new ConstantPriceOracle(coreAddress, 1e18); + usdcConstantOracle = new ConstantPriceOracle(coreAddress, 1e30); address[] memory proposerCancellerAddresses = new address[](3); proposerCancellerAddresses[0] = guardianAddresses.pcvGuardAddress1; @@ -210,6 +222,7 @@ contract SystemUnitTest is Test { vm.startPrank(addresses.governorAddress); core.grantPCVController(address(pcvGuardian)); + core.grantPCVController(address(pcvRouter)); core.grantPCVController(address(allocator)); core.grantPCVGuard(addresses.userAddress); @@ -225,8 +238,11 @@ contract SystemUnitTest is Test { core.grantRateLimitedRedeemer(address(daipsm)); core.grantRateLimitedRedeemer(address(usdcpsm)); + core.grantLocker(address(pcvDepositUsdc)); + core.grantLocker(address(pcvDepositDai)); core.grantLocker(address(pcvGuardian)); core.grantLocker(address(allocator)); + core.grantLocker(address(pcvOracle)); core.grantLocker(address(usdcpsm)); core.grantLocker(address(daipsm)); core.grantLocker(address(entry)); @@ -250,6 +266,20 @@ contract SystemUnitTest is Test { allocator.connectDeposit(address(usdcpsm), address(pcvDepositUsdc)); allocator.connectDeposit(address(daipsm), address(pcvDepositDai)); + + /// Configure PCV Oracle + address[] memory venues = new address[](2); + venues[0] = address(pcvDepositDai); + venues[1] = address(pcvDepositUsdc); + + address[] memory oracles = new address[](2); + oracles[0] = address(daiConstantOracle); + oracles[1] = address(usdcConstantOracle); + + pcvOracle.addVenues(venues, oracles); + + core.setPCVOracle(pcvOracle); + vm.stopPrank(); /// top up contracts with tokens for testing @@ -265,6 +295,9 @@ contract SystemUnitTest is Test { vm.label(address(pcvDepositDai), "pcvDepositDai"); vm.label(address(pcvDepositUsdc), "pcvDepositUsdc"); vm.label(address(this), "address this"); + vm.label(address(dai), "DAI"); + vm.label(address(usdc), "USDC"); + // console.log("finished setting up system"); } function testSetup() public { @@ -390,6 +423,9 @@ contract SystemUnitTest is Test { assertTrue(core.isPCVGuard(addresses.secondUserAddress)); assertTrue(core.isGuardian(address(pcvGuardian))); + + assertTrue(pcvOracle.isVenue(address(pcvDepositDai))); + assertTrue(pcvOracle.isVenue(address(pcvDepositUsdc))); } function testPCVGuardWithdrawAllToSafeAddress() public { @@ -618,9 +654,45 @@ contract SystemUnitTest is Test { address(mock), address(mock) ); + vm.label(address(deposit), "Malicious Morpho Compound PCV Deposit"); - vm.prank(addresses.governorAddress); + /// Configure PCV Oracle + address[] memory venues = new address[](1); + venues[0] = address(deposit); + + address[] memory oracles = new address[](1); + oracles[0] = address(usdcConstantOracle); + + /// call to morpo update indexes does nothing at first + mock.setResponseToCall( + address(0), + "", + "", + bytes4(keccak256("updateP2PIndexes(address)")) + ); + + /// call to accrue returns 0 + mock.setResponseToCall( + address(0), + "", + abi.encode(0), + bytes4(keccak256("accrue(address)")) + ); + + /// call to getCurrentSupplyBalanceInOf returns 0 + mock.setResponseToCall( + address(0), + "", + abi.encode(0, 0, 0), + bytes4(keccak256("getCurrentSupplyBalanceInOf(address,address)")) + ); + + vm.startPrank(addresses.governorAddress); core.grantLocker(address(deposit)); + core.createRole(VoltRoles.PCV_DEPOSIT, VoltRoles.GOVERNOR); + core.grantRole(VoltRoles.PCV_DEPOSIT, address(deposit)); + pcvOracle.addVenues(venues, oracles); + vm.stopPrank(); /// call to morpo update indexes attempts reentry mock.setResponseToCall( diff --git a/test/unit/utils/Deviation.sol b/test/unit/utils/Deviation.sol index bdfd952b3..dcf68e6c1 100644 --- a/test/unit/utils/Deviation.sol +++ b/test/unit/utils/Deviation.sol @@ -60,6 +60,17 @@ library Deviation { return basisPoints; } + /// @notice return the percent deviation between a and b in wei. 1 eth = 100% + function calculateDeviationEthGranularity( + int256 a, + int256 b + ) internal pure returns (int256) { + int256 delta = a - b; + int256 basisPoints = (delta * Constants.ETH_GRANULARITY_INT) / a; + + return basisPoints; + } + /// @notice function to return whether or not the new price is within /// the acceptable deviation threshold function isWithinDeviationThreshold( diff --git a/test/unit/vcon/MarketGovernance.t.sol b/test/unit/vcon/MarketGovernance.t.sol new file mode 100644 index 000000000..adc8c9a3b --- /dev/null +++ b/test/unit/vcon/MarketGovernance.t.sol @@ -0,0 +1,273 @@ +pragma solidity 0.8.13; + +import {console} from "@forge-std/console.sol"; +import {MockERC20} from "@test/mock/MockERC20.sol"; +import {VoltRoles} from "@voltprotocol/core/VoltRoles.sol"; +import {MockPCVSwapper} from "@test/mock/MockPCVSwapper.sol"; +import {SystemUnitTest} from "@test/unit/system/System.t.sol"; +import {MockPCVDepositV3} from "@test/mock/MockPCVDepositV3.sol"; +import {MarketGovernance} from "@voltprotocol/vcon/MarketGovernance.sol"; +import {TestAddresses as addresses} from "@test/unit/utils/TestAddresses.sol"; + +contract UnitTestMarketGovernance is SystemUnitTest { + MarketGovernance public mgov; + MockPCVSwapper public pcvSwapper; + address public venue = address(10); /// a in hex + uint256 public profitToVconRatio = 5; /// for each 1 wei in profit, 5 wei of vcon is received + uint256 public daiDepositAmount = 1_000_000e18; + uint256 public usdcDepositAmount = 1_000_000e6; + + uint256 public vconDepositAmount = 1_000_000e18; + + function setUp() public override { + super.setUp(); + + /// can only swap from dai to usdc + pcvSwapper = new MockPCVSwapper( + MockERC20(address(dai)), + MockERC20(address(usdc)) + ); + pcvSwapper.mockSetExchangeRate(1e6); /// set dai->usdc exchange rate + + mgov = new MarketGovernance(coreAddress); + + vm.startPrank(addresses.governorAddress); + + mgov.setProfitToVconRatio(venue, profitToVconRatio); + + core.grantPCVController(address(mgov)); + core.grantLocker(address(mgov)); + + address[] memory swapper = new address[](1); + swapper[0] = address(pcvSwapper); + mgov.addPCVSwappers(swapper); + + vm.stopPrank(); + } + + function _initializeVenues() private { + mgov.initializeVenue(address(pcvDepositDai)); + mgov.initializeVenue(address(pcvDepositUsdc)); + + pcvDepositUsdc.setLastRecordedProfit(10_000e18); + pcvDepositDai.setLastRecordedProfit(10_000e18); + } + + function testMarketGovernanceSetup() public { + assertEq(address(mgov.core()), coreAddress); + + assertEq(mgov.profitToVconRatio(address(0)), 0); + assertEq(mgov.profitToVconRatio(venue), profitToVconRatio); + + assertTrue(core.isLocker(address(mgov))); + assertTrue(core.isPCVController(address(mgov))); + } + + /// steps: + //// define mock swapper + //// add dai -> usdc and usdc -> dai routes to mgov contract + + function testSystemOneUser() public { + _initializeVenues(); + + dai.mint(address(pcvDepositDai), daiDepositAmount); + usdc.mint(address(pcvDepositUsdc), usdcDepositAmount); + + entry.deposit(address(pcvDepositDai)); + entry.deposit(address(pcvDepositUsdc)); + + vcon.mint(address(this), vconDepositAmount); + + vcon.approve(address(mgov), vconDepositAmount); + mgov.deposit( + vconDepositAmount, + pcvDepositDai.balance(), + address(pcvDepositDai), + address(pcvDepositUsdc), + address(pcvSwapper) + ); + + assertEq( + pcvOracle.getTotalPcv(), + pcvOracle.getVenueBalance(address(pcvDepositUsdc)) + ); + } + + function testSystemTwoUsers() public { + _initializeVenues(); + + uint256 totalPCV = pcvOracle.getTotalPcv(); + + // console.log("dai deposit balance: "); + // console.logInt(mgov.getVenueBalance(address(pcvDepositDai), totalPCV)); + + // console.log("usdc deposit balance: "); + // console.logInt(mgov.getVenueBalance(address(pcvDepositUsdc), totalPCV)); + + testSystemOneUser(); + + totalPCV = pcvOracle.getTotalPcv(); + + // console.log("dai deposit balance: "); + // console.logInt(mgov.getVenueBalance(address(pcvDepositDai), totalPCV)); + + // console.log("usdc deposit balance: "); + // console.logInt(mgov.getVenueBalance(address(pcvDepositUsdc), totalPCV)); + + address user = address(1000); + + vm.startPrank(user); + + vcon.mint(user, vconDepositAmount); + vcon.approve(address(mgov), vconDepositAmount); + mgov.deposit( + vconDepositAmount, + pcvDepositUsdc.balance() / 2, + address(pcvDepositUsdc), + address(pcvDepositDai), + address(pcvSwapper) + ); + + vm.stopPrank(); + + assertEq(vcon.balanceOf(user), 0); + assertEq( + pcvOracle.getTotalPcv() / 2, + pcvOracle.getVenueBalance(address(pcvDepositUsdc)) + ); + assertEq( + pcvOracle.getTotalPcv() / 2, + pcvOracle.getVenueBalance(address(pcvDepositDai)) + ); + } + + function testSystemThreeUsersLastNoDeposit(uint120 vconAmount) public { + _initializeVenues(); + + vm.assume(vconAmount > 1e9); + testSystemTwoUsers(); + + address user = address(1001); + + uint256 startingTotalSupply = mgov.totalSupply(); + + vm.startPrank(user); + + vcon.mint(user, vconAmount); + vcon.approve(address(mgov), vconAmount); + mgov.depositNoMove(address(pcvDepositUsdc), vconAmount); + + vm.stopPrank(); + + assertEq(vcon.balanceOf(user), 0); + + uint256 endingTotalSupply = mgov.totalSupply(); + uint256 totalPCV = pcvOracle.getTotalPcv(); + + // console.log("\nvenue balance usdc"); + // console.logInt(mgov.getVenueBalance(address(pcvDepositUsdc), totalPCV)); + // console.log("------------------\n"); + + // console.log("\nvenue balance dai"); + // console.logInt(mgov.getVenueBalance(address(pcvDepositDai), totalPCV)); + // console.log("------------------\n"); + + assertEq(endingTotalSupply, startingTotalSupply + vconAmount); + assertTrue(mgov.getVenueBalance(address(pcvDepositUsdc), totalPCV) < 0); /// underweight USDC balance + assertTrue(mgov.getVenueBalance(address(pcvDepositDai), totalPCV) > 0); /// overweight DAI balance + } + + function testSystemThreeUsersLastNoDepositIndividual() public { + _initializeVenues(); + + uint120 vconAmount = 1e9; + + testSystemTwoUsers(); + + address user = address(1001); + + uint256 startingTotalSupply = mgov.totalSupply(); + + vm.startPrank(user); + + vcon.mint(user, vconAmount); + vcon.approve(address(mgov), vconAmount); + mgov.depositNoMove(address(pcvDepositUsdc), vconAmount); + + vm.stopPrank(); + + assertEq(vcon.balanceOf(user), 0); + + uint256 endingTotalSupply = mgov.totalSupply(); + uint256 totalPCV = pcvOracle.getTotalPcv(); + + // console.log("\nvenue balance usdc"); + // console.logInt(mgov.getVenueBalance(address(pcvDepositUsdc), totalPCV)); + // console.log("------------------\n"); + + // console.log("\nvenue balance dai"); + // console.logInt(mgov.getVenueBalance(address(pcvDepositDai), totalPCV)); + // console.log("------------------\n"); + + assertEq(endingTotalSupply, startingTotalSupply + vconAmount); + assertTrue(mgov.getVenueBalance(address(pcvDepositUsdc), totalPCV) < 0); /// underweight USDC balance + assertTrue(mgov.getVenueBalance(address(pcvDepositDai), totalPCV) > 0); /// overweight DAI balance + } + + function testUserDepositsNoMove(uint120 vconAmount) public { + _initializeVenues(); + + vm.assume(vconAmount > 1e9); + + address user = address(1001); + + uint256 startingTotalSupply = mgov.totalSupply(); + + vm.startPrank(user); + + vcon.mint(user, vconAmount); + vcon.approve(address(mgov), vconAmount); + mgov.depositNoMove(address(pcvDepositUsdc), vconAmount); + + vm.stopPrank(); + + assertEq(vcon.balanceOf(user), 0); + + uint256 endingTotalSupply = mgov.totalSupply(); + uint256 totalPCV = pcvOracle.getTotalPcv(); + + assertEq(endingTotalSupply, startingTotalSupply + vconAmount); + assertTrue(mgov.getVenueBalance(address(pcvDepositUsdc), totalPCV) < 0); /// underweight USDC + assertTrue(mgov.getVenueBalance(address(pcvDepositDai), totalPCV) > 0); /// overweight DAI + } + + function testUserDepositsFailsNotInitialized() public { + mgov.initializeVenue(address(pcvDepositUsdc)); + + address user = address(1001); + uint120 vconAmount = 1e18; + + vm.startPrank(user); + vcon.mint(user, vconAmount); + vcon.approve(address(mgov), vconAmount); + mgov.depositNoMove(address(pcvDepositUsdc), vconAmount); + vm.stopPrank(); + + vm.expectRevert("MarketGovernance: venue not initialized"); + mgov.deposit( + vconAmount, + 1e18, + address(pcvDepositUsdc), + address(pcvDepositDai), + address(pcvSwapper) + ); + + vm.expectRevert("MarketGovernance: venue not initialized"); + mgov.depositNoMove(address(pcvDepositDai), vconAmount); + } + + /// todo test withdrawing + /// todo test depositing fails when venue not initialized + /// todo test withdrawing when there are profits + /// todo test withdrawing when there are losses +} From c571e27e64e29935cb6fb6755bb972f9a8bdcd46 Mon Sep 17 00:00:00 2001 From: Elliot Date: Tue, 24 Jan 2023 00:01:29 -0800 Subject: [PATCH 27/74] market gov tests --- test/unit/vcon/MarketGovernance.t.sol | 60 ++++++--------------------- 1 file changed, 12 insertions(+), 48 deletions(-) diff --git a/test/unit/vcon/MarketGovernance.t.sol b/test/unit/vcon/MarketGovernance.t.sol index adc8c9a3b..33de88be0 100644 --- a/test/unit/vcon/MarketGovernance.t.sol +++ b/test/unit/vcon/MarketGovernance.t.sol @@ -94,26 +94,12 @@ contract UnitTestMarketGovernance is SystemUnitTest { } function testSystemTwoUsers() public { - _initializeVenues(); - uint256 totalPCV = pcvOracle.getTotalPcv(); - // console.log("dai deposit balance: "); - // console.logInt(mgov.getVenueBalance(address(pcvDepositDai), totalPCV)); - - // console.log("usdc deposit balance: "); - // console.logInt(mgov.getVenueBalance(address(pcvDepositUsdc), totalPCV)); - testSystemOneUser(); totalPCV = pcvOracle.getTotalPcv(); - // console.log("dai deposit balance: "); - // console.logInt(mgov.getVenueBalance(address(pcvDepositDai), totalPCV)); - - // console.log("usdc deposit balance: "); - // console.logInt(mgov.getVenueBalance(address(pcvDepositUsdc), totalPCV)); - address user = address(1000); vm.startPrank(user); @@ -142,8 +128,6 @@ contract UnitTestMarketGovernance is SystemUnitTest { } function testSystemThreeUsersLastNoDeposit(uint120 vconAmount) public { - _initializeVenues(); - vm.assume(vconAmount > 1e9); testSystemTwoUsers(); @@ -164,78 +148,51 @@ contract UnitTestMarketGovernance is SystemUnitTest { uint256 endingTotalSupply = mgov.totalSupply(); uint256 totalPCV = pcvOracle.getTotalPcv(); - // console.log("\nvenue balance usdc"); - // console.logInt(mgov.getVenueBalance(address(pcvDepositUsdc), totalPCV)); - // console.log("------------------\n"); - - // console.log("\nvenue balance dai"); - // console.logInt(mgov.getVenueBalance(address(pcvDepositDai), totalPCV)); - // console.log("------------------\n"); - assertEq(endingTotalSupply, startingTotalSupply + vconAmount); assertTrue(mgov.getVenueBalance(address(pcvDepositUsdc), totalPCV) < 0); /// underweight USDC balance assertTrue(mgov.getVenueBalance(address(pcvDepositDai), totalPCV) > 0); /// overweight DAI balance } function testSystemThreeUsersLastNoDepositIndividual() public { - _initializeVenues(); - - uint120 vconAmount = 1e9; - testSystemTwoUsers(); + uint120 vconAmount = 1e9; address user = address(1001); - uint256 startingTotalSupply = mgov.totalSupply(); vm.startPrank(user); - vcon.mint(user, vconAmount); vcon.approve(address(mgov), vconAmount); mgov.depositNoMove(address(pcvDepositUsdc), vconAmount); - vm.stopPrank(); - assertEq(vcon.balanceOf(user), 0); - uint256 endingTotalSupply = mgov.totalSupply(); uint256 totalPCV = pcvOracle.getTotalPcv(); - // console.log("\nvenue balance usdc"); - // console.logInt(mgov.getVenueBalance(address(pcvDepositUsdc), totalPCV)); - // console.log("------------------\n"); - - // console.log("\nvenue balance dai"); - // console.logInt(mgov.getVenueBalance(address(pcvDepositDai), totalPCV)); - // console.log("------------------\n"); - + assertEq(vcon.balanceOf(user), 0); assertEq(endingTotalSupply, startingTotalSupply + vconAmount); assertTrue(mgov.getVenueBalance(address(pcvDepositUsdc), totalPCV) < 0); /// underweight USDC balance assertTrue(mgov.getVenueBalance(address(pcvDepositDai), totalPCV) > 0); /// overweight DAI balance } function testUserDepositsNoMove(uint120 vconAmount) public { - _initializeVenues(); - vm.assume(vconAmount > 1e9); - address user = address(1001); + _initializeVenues(); + address user = address(1001); uint256 startingTotalSupply = mgov.totalSupply(); vm.startPrank(user); - vcon.mint(user, vconAmount); vcon.approve(address(mgov), vconAmount); mgov.depositNoMove(address(pcvDepositUsdc), vconAmount); - vm.stopPrank(); - assertEq(vcon.balanceOf(user), 0); - uint256 endingTotalSupply = mgov.totalSupply(); uint256 totalPCV = pcvOracle.getTotalPcv(); + assertEq(vcon.balanceOf(user), 0); assertEq(endingTotalSupply, startingTotalSupply + vconAmount); assertTrue(mgov.getVenueBalance(address(pcvDepositUsdc), totalPCV) < 0); /// underweight USDC assertTrue(mgov.getVenueBalance(address(pcvDepositDai), totalPCV) > 0); /// overweight DAI @@ -266,6 +223,13 @@ contract UnitTestMarketGovernance is SystemUnitTest { mgov.depositNoMove(address(pcvDepositDai), vconAmount); } + function testReinitializingVenueFails() public { + _initializeVenues(); + + vm.expectRevert("MarketGovernance: venue already has share price"); + mgov.initializeVenue(address(pcvDepositUsdc)); + } + /// todo test withdrawing /// todo test depositing fails when venue not initialized /// todo test withdrawing when there are profits From 6732e39de68c3c662c7cb01be3fcf62f8f7f72c0 Mon Sep 17 00:00:00 2001 From: Elliot Date: Wed, 25 Jan 2023 09:23:33 -0800 Subject: [PATCH 28/74] Simplify interface --- src/utils/DeviationWeiGranularity.sol | 23 ++ src/vcon/IMarketGovernance.sol | 90 ++++++ src/vcon/MarketGovernance.sol | 432 +++++++++++++++++--------- test/unit/pcv/PCVRouter.t.sol | 2 + test/unit/utils/Deviation.sol | 11 - test/unit/vcon/MarketGovernance.t.sol | 178 +++++++++-- 6 files changed, 550 insertions(+), 186 deletions(-) create mode 100644 src/utils/DeviationWeiGranularity.sol create mode 100644 src/vcon/IMarketGovernance.sol diff --git a/src/utils/DeviationWeiGranularity.sol b/src/utils/DeviationWeiGranularity.sol new file mode 100644 index 000000000..c237b548d --- /dev/null +++ b/src/utils/DeviationWeiGranularity.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.13; + +import {Constants} from "@voltprotocol/Constants.sol"; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; + +/// @title contract that determines whether or not a new value is within +/// an acceptable deviation threshold +/// @author Elliot Friedman +library DeviationWeiGranularity { + using SafeCast for *; + + /// @notice return the percent deviation between a and b in wei. 1 eth = 100% + function calculateDeviation( + int256 a, + int256 b + ) internal pure returns (int256) { + int256 delta = a - b; + int256 basisPoints = (delta * Constants.ETH_GRANULARITY_INT) / a; + + return basisPoints; + } +} diff --git a/src/vcon/IMarketGovernance.sol b/src/vcon/IMarketGovernance.sol new file mode 100644 index 000000000..078b84c64 --- /dev/null +++ b/src/vcon/IMarketGovernance.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.13; + +interface IMarketGovernance { + /// @notice struct that tracks a PCV movement + /// @param source pcv deposit to pull funds from + /// @param destination pcv deposit to send funds + /// @param swapper address to swap tokens + /// @param amountPcv to move from src to dest + struct Rebalance { + address source; + address destination; + address swapper; + uint256 amountPcv; + } + + /// ---------- Events ---------- + + /// @notice event emitted when a user stakes their VCON + event VconStaked( + address indexed user, + uint256 timestamp, + uint256 vconAmount + ); + + /// @notice event emitted when a user unstakes their VCON + event VconUnstaked( + address indexed user, + uint256 timestamp, + uint256 vconAmount + ); + + /// ---------- Permissionless User PCV Allocation Methods ---------- + + /// this function can be called with amount of PCV set to 0 + /// @param amountVcon to stake + /// @param amountPcv to move from src to dest + /// @param source pcv deposit to pull funds from + /// @param destination pcv deposit to send funds + /// @param swapper address to swap tokens + function stake( + uint256 amountVcon, + uint256 amountPcv, + address source, + address destination, + address swapper + ) external; + + /// @notice this function automatically calculates + /// the amount of PCV to remove from the source + /// based on the user's total amount of staked VCON + /// @param amountVcon to stake + /// @param source pcv deposit to pull funds from + /// @param destination pcv deposit to send funds + /// @param swapper address to swap tokens + /// @param vconRecipient address to receive VCON tokens + function unstake( + uint256 amountVcon, + address source, + address destination, + address swapper, + address vconRecipient + ) external; + + /// @notice permissionlessly rebalance PCV based on + /// the pre-existing VCON weights. Each movement must make + /// the system more balanced, otherwise it will revert + /// causing the entire transaction to fail. + /// @param movements of PCV between venues + function rebalance(Rebalance[] calldata movements) external; + + /// apply the amount of rewards a user has accrued, sending directly to their account + /// each venue will have the accrue function called in order to get the most up to + /// date pnl from them + function applyRewards(address[] calldata venues, address user) external; + + /// return the total amount of rewards a user is entitled to + /// this value will usually be stale as .accrue() must be called in the same block/tx as this function + /// for it to return the proper amount of profit + function getAccruedRewards( + address[] calldata venues, + address user + ) external view returns (int256); + + /// ---------- Initialize Method ---------- + + /// @notice permissionlessly initialize a venue + /// required to be able to utilize a given PCV Deposit in market governance + function initializeVenue(address venue) external; +} diff --git a/src/vcon/MarketGovernance.sol b/src/vcon/MarketGovernance.sol index 6d28e4bda..c9c443583 100644 --- a/src/vcon/MarketGovernance.sol +++ b/src/vcon/MarketGovernance.sol @@ -7,16 +7,28 @@ import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import {PCVMover} from "@voltprotocol/pcv/PCVMover.sol"; import {Constants} from "@voltprotocol/Constants.sol"; import {CoreRefV2} from "@voltprotocol/refs/CoreRefV2.sol"; -import {Deviation} from "@test/unit/utils/Deviation.sol"; +import {IPCVOracle} from "@voltprotocol/oracle/IPCVOracle.sol"; +import {IPCVDeposit} from "@voltprotocol/pcv/IPCVDeposit.sol"; import {IPCVDepositV2} from "@voltprotocol/pcv/IPCVDepositV2.sol"; +import {IMarketGovernance} from "@voltprotocol/vcon/IMarketGovernance.sol"; +import {DeviationWeiGranularity} from "@voltprotocol/utils/DeviationWeiGranularity.sol"; import {console} from "@forge-std/console.sol"; /// @notice this contract requires the PCV Controller and Locker role +/// /// Core formula for market governance rewards: /// Profit Per VCON = Profit Per VCON + ∆Cumulative Profits (Dollars) * VCON:Dollar / VCON Staked -contract MarketGovernance is CoreRefV2, PCVMover { - using Deviation for *; +/// +/// If an account has an unrealized loss on a venue, they cannot do any other action on that venue +/// until they have called the function realizeLosses and marked down the amount of VCON they have staked +/// on that venue. Once the loss has been marked down, they can proceed with other actions. +/// +/// The VCON:Dollar ratio is the same for both profits and losses. If a venue has a VCON:Dollar ratio of 5:1 +/// and the venue gains $5 in profits, then 25 VCON will be distributed across all VCON stakers in that venue. +/// If that same venue losses $5, then a loss of 25 VCON will be distributed across all VCON stakers in that venue. +contract MarketGovernance is CoreRefV2, PCVMover, IMarketGovernance { + using DeviationWeiGranularity for *; using SafeERC20 for *; using SafeCast for *; @@ -35,7 +47,7 @@ contract MarketGovernance is CoreRefV2, PCVMover { ); /// @notice total amount of VCON deposited across all venues - uint256 public totalSupply; + uint256 public vconStaked; /// @dev convention for all normal mappings is key (venue -> value) @@ -46,18 +58,18 @@ contract MarketGovernance is CoreRefV2, PCVMover { /// TODO simplify this contract down to only track venue profit, /// and do the conversion to VCON at the end when accruing rewards - /// alternatively, pack lastRecordedVconPricePerVenue, - /// lastRecordedProfit and totalVenueDepositedVcon into a single slot for gas optimization + /// alternatively, pack venueLastRecordedSharePrice, + /// venueLastRecordedProfit and venueVconDeposited into a single slot for gas optimization /// @notice last recorded profit index per venue - mapping(address => uint128) public lastRecordedProfit; + mapping(address => uint128) public venueLastRecordedProfit; /// @notice last recorded share price of a venue in VCON /// starts off at 1e18 - mapping(address => uint128) public lastRecordedVconPricePerVenue; + mapping(address => uint128) public venueLastRecordedSharePrice; /// @notice total vcon deposited per venue - mapping(address => uint256) public totalVenueDepositedVcon; + mapping(address => uint256) public venueVconDeposited; /// ---------- Per Venue User Profit Tracking ---------- @@ -65,11 +77,11 @@ contract MarketGovernance is CoreRefV2, PCVMover { /// @notice record of VCON index when user joined a given venue mapping(address => mapping(address => uint256)) - public startingVenueVconProfit; + public venueUserStartingVconProfit; /// @notice record how much VCON a user deposited in a given venue mapping(address => mapping(address => uint256)) - public userVenueDepositedVcon; + public venueUserDepositedVcon; /// @param _core reference to core constructor(address _core) CoreRefV2(_core) {} @@ -79,13 +91,13 @@ contract MarketGovernance is CoreRefV2, PCVMover { function initializeVenue(address venue) external globalLock(1) { require(pcvOracle().isVenue(venue), "MarketGovernance: invalid venue"); - uint256 startingLastRecordedProfit = lastRecordedProfit[venue]; + uint256 startingLastRecordedProfit = venueLastRecordedProfit[venue]; require( startingLastRecordedProfit == 0, "MarketGovernance: profit already recorded" ); require( - lastRecordedVconPricePerVenue[venue] == 0, + venueLastRecordedSharePrice[venue] == 0, "MarketGovernance: venue already has share price" ); @@ -94,148 +106,146 @@ contract MarketGovernance is CoreRefV2, PCVMover { uint256 endingLastRecordedProfit = IPCVDepositV2(venue) .lastRecordedProfit(); - lastRecordedProfit[venue] = endingLastRecordedProfit.toUint128(); - lastRecordedVconPricePerVenue[venue] = Constants + venueLastRecordedProfit[venue] = endingLastRecordedProfit.toUint128(); + venueLastRecordedSharePrice[venue] = Constants .ETH_GRANULARITY .toUint128(); } - /// ---------- Permissionless User PCV Allocation Methods ---------- - /// TODO update pcv oracle to cache total pcv so getAllPCV isn't needed to figure /// out if weights are correct + + /// ---------- Permissionless User PCV Allocation Methods ---------- + + /// any losses or gains are applied to venueLastRecordedSharePrice + /// + /// + /// user deposits + + /// @notice a user can get slashed up to their full VCON stake for entering + /// a venue that takes a loss. /// @param amountVcon to stake on destination /// @param amountPcv to move from source to destination /// @param source address to pull funds from /// @param destination address to accrue rewards to, and send funds to /// @param swapper address that swaps asset types between src and dest if needed - function deposit( + function stake( uint256 amountVcon, uint256 amountPcv, address source, address destination, address swapper ) external globalLock(1) { + IPCVOracle oracle = pcvOracle(); + require(oracle.isVenue(source), "MarketGovernance: invalid source"); + require( + oracle.isVenue(destination), + "MarketGovernance: invalid destination" + ); + require(source != destination, "MarketGovernance: src and dest equal"); + _accrue(destination); /// update profitPerVCON in the destination so the user gets in at the current share price - uint256 vconRewards = _harvestRewards(msg.sender, destination); /// auto-compound rewards + int256 vconRewards = _harvestRewards(msg.sender, destination); /// auto-compound rewards + require( + vconRewards >= 0, + "MarketGovernance: must realize loss before staking" + ); + /// check and an interaction with a trusted contract vcon().safeTransferFrom(msg.sender, address(this), amountVcon); /// transfer VCON in - uint256 totalVconDeposited = amountVcon + vconRewards; /// auto-compound rewards + uint256 totalVconDeposited = amountVcon + vconRewards.toUint256(); /// auto-compound rewards /// global updates - totalSupply += totalVconDeposited; + vconStaked += totalVconDeposited; /// user updates - userVenueDepositedVcon[destination][msg.sender] += totalVconDeposited; + venueUserDepositedVcon[destination][msg.sender] += totalVconDeposited; /// venue updates - totalVenueDepositedVcon[destination] += totalVconDeposited; + venueVconDeposited[destination] += totalVconDeposited; /// ignore balance checks if only one user is allocating in the system - bool ignoreBalanceChecks = totalSupply == totalVconDeposited; - - _movePCVWithChecks( - source, - destination, - swapper, - amountPcv, - ignoreBalanceChecks - ); - } - - /// @notice deposit VCON without moving PCV - /// @param venue to stake VCON on - /// @param amountVcon amount of VCON to stake - function depositNoMove( - address venue, - uint256 amountVcon - ) external globalLock(1) { - /// update profitPerVCON in the destination so the user buys - /// in at the current share price - /// interaction, unfortunately it is necessary to do this before updating the user's balance - /// in order to ensure the user gets the current venue price when they stake - _accrue(venue); - - uint256 vconRewards = _harvestRewards(msg.sender, venue); /// auto-compound rewards - - uint256 totalVconDeposited = amountVcon + vconRewards; /// auto-compound rewards - - /// effects - - /// global updates - totalSupply += totalVconDeposited; + bool ignoreBalanceChecks = vconStaked == totalVconDeposited; - /// user updates - userVenueDepositedVcon[venue][msg.sender] += totalVconDeposited; - - /// venue updates - totalVenueDepositedVcon[venue] += totalVconDeposited; - - /// interactions - - /// check and an interaction with a trusted contract - vcon().safeTransferFrom(msg.sender, address(this), amountVcon); /// transfer VCON in + if (amountPcv != 0) { + _movePCVWithChecks( + source, + destination, + swapper, + amountPcv, + ignoreBalanceChecks + ); + } } - /// TODO add a function that decouples the movement of PCV from the - /// depositing so users can deposit without paying huge gas costs - /// @notice unstake VCON and transfer corresponding VCON to another venue /// @param amountVcon the amount of VCON staked to unstake - /// @param amountPcv the amount of PCV to move from source /// @param source address to accrue rewards to, and pull funds from /// @param destination address to send funds + /// @param swapper address to swap funds through /// @param vconRecipient address to receive the VCON - function withdraw( + function unstake( uint256 amountVcon, - uint256 amountPcv, address source, address destination, address swapper, address vconRecipient ) external globalLock(1) { - /// ---------- Check ---------- + /// ---------- Checks ---------- + IPCVOracle oracle = pcvOracle(); + + require(oracle.isVenue(source), "MarketGovernance: invalid source"); + require( + oracle.isVenue(destination), + "MarketGovernance: invalid destination" + ); require(source != destination, "MarketGovernance: src and dest equal"); /// ---------- Effects ---------- _accrue(source); /// update profitPerVCON in the source so the user gets paid out at the current share price - { - uint256 vconRewards = _harvestRewards(msg.sender, source); /// pay msg.sender their rewards - require( - userVenueDepositedVcon[source][msg.sender] + vconRewards >= - amountVcon, - "MarketGovernance: invalid vcon amount" - ); + /// amount of PCV to withdraw is the amount vcon * venue balance / total vcon staked on venue + uint256 amountPcv = getProRataPCVAmounts(source, amountVcon); - /// global updates - totalSupply -= amountVcon; - - /// user updates - /// balance = 80 - /// amount = 100 - /// rewards = 30 - /// amount deducted = 70 - /// _____________ - /// balance = 10 - userVenueDepositedVcon[source][msg.sender] -= - amountVcon - - vconRewards; - } + int256 vconRewards = _harvestRewards(msg.sender, source); /// pay msg.sender their rewards + uint256 vconReward = vconRewards.toUint256(); /// pay msg.sender their rewards + + require( + vconRewards >= 0, + "MarketGovernance: must realize loss before unstaking" + ); + + require( + venueUserDepositedVcon[source][msg.sender] + vconReward >= + amountVcon, + "MarketGovernance: invalid vcon amount" + ); + + /// global updates + vconStaked -= amountVcon; + + /// user updates + /// balance = 80 + /// amount = 100 + /// rewards = 30 + /// amount deducted = 70 + /// _____________ + /// balance = 10 + venueUserDepositedVcon[source][msg.sender] -= amountVcon - vconReward; /// venue updates - totalVenueDepositedVcon[destination] -= amountVcon; + venueVconDeposited[source] -= amountVcon; /// ---------- Interactions ---------- vcon().safeTransfer(vconRecipient, amountVcon); /// transfer VCON to recipient - /// ignore balance checks if only one user is allocating in the system - bool ignoreBalanceChecks = totalSupply == - userVenueDepositedVcon[source][msg.sender]; + /// ignore balance checks if only one user is in the system and is allocating to a single venue + bool ignoreBalanceChecks = vconStaked == + venueUserDepositedVcon[source][msg.sender]; _movePCVWithChecks( source, @@ -246,35 +256,12 @@ contract MarketGovernance is CoreRefV2, PCVMover { ); } - /// @notice rebalance PCV without staking or unstaking VCON - /// @param source address to pull funds from - /// @param destination recipient address for funds - /// @param swapper address that swaps denominations if necessary - /// @param amountPcv the amount of PCV to move from source - function rebalance( - address source, - address destination, - address swapper, - uint256 amountPcv - ) external globalLock(1) { - _movePCVWithChecks(source, destination, swapper, amountPcv, false); - } - - struct Rebalance { - address source; - address destination; - address swapper; - uint256 amountPcv; - } - /// @notice rebalance PCV without staking or unstaking VCON /// each individual action must make the system more balanced /// as a whole, otherwise it will revert /// @param movements information on all pcv movements /// including sources, destinations, amounts and swappers - function rebalanceBulk( - Rebalance[] calldata movements - ) external globalLock(1) { + function rebalance(Rebalance[] calldata movements) external globalLock(1) { unchecked { for (uint256 i = 0; i < movements.length; i++) { address source = movements[i].source; @@ -319,14 +306,14 @@ contract MarketGovernance is CoreRefV2, PCVMover { address venue, uint256 totalPcv ) public view returns (int256) { - uint256 venueDepositedVcon = totalVenueDepositedVcon[venue]; + uint256 venueDepositedVcon = venueVconDeposited[venue]; uint256 venueBalance = pcvOracle().getVenueBalance(venue); - uint256 cachedTotalSupply = totalSupply; + uint256 cachedVconStaked = vconStaked; /// 0 checks as any 0 denominator will cause a revert if ( (venueDepositedVcon == 0 && venueBalance == 0) || - cachedTotalSupply == 0 || + cachedVconStaked == 0 || totalPcv == 0 ) { return 0; /// perfectly balanced at 0 PCV or 0 VCON staked @@ -335,7 +322,7 @@ contract MarketGovernance is CoreRefV2, PCVMover { /// Step 1. /// find out actual ratio of VCON in a given venue based on total VCON staked uint256 venueDepositedVconRatio = (venueDepositedVcon * - Constants.ETH_GRANULARITY) / cachedTotalSupply; + Constants.ETH_GRANULARITY) / cachedVconStaked; /// perfectly balanced if (venueDepositedVconRatio == 0 && venueBalance == 0) { @@ -346,7 +333,7 @@ contract MarketGovernance is CoreRefV2, PCVMover { /// add this 0 check because deviation divides by a and would cause a revert /// replicate step 3 if no VCON is deposited by comparing pcv to venue balance return - Deviation.calculateDeviationEthGranularity( + DeviationWeiGranularity.calculateDeviation( totalPcv.toInt256(), venueBalance.toInt256() ); @@ -362,17 +349,138 @@ contract MarketGovernance is CoreRefV2, PCVMover { /// Step 3. /// if venue deposited VCON, return the ratio between expected pcv vs actual pcv return - -Deviation.calculateDeviationEthGranularity( + -DeviationWeiGranularity.calculateDeviation( expectedPcvAmount.toInt256(), venueBalance.toInt256() ); } + /// @param venue to figure out total pro rata pcv + /// @param amountVcon to find total amount of pro rata pcv + /// @return the pro rata pcv controll ed in the given venue based on the amount of VCON + function getProRataPCVAmounts( + address venue, + uint256 amountVcon + ) public view returns (uint256) { + uint256 venuePcv = IPCVDeposit(venue).balance(); + uint256 cachedVconStaked = venueVconDeposited[venue]; + + console.log("cachedVconStaked: ", cachedVconStaked); + + /// 0 checks as any 0 denominator will cause a revert + if (cachedVconStaked == 0) { + return 0; /// perfectly balanced at 0 PCV or 0 VCON staked + } + + uint256 proRataPcv = (amountVcon * venuePcv) / cachedVconStaked; + + return proRataPcv; + } + struct PCVDepositInfo { address deposit; uint256 amount; } + /// apply the amount of rewards a user has accrued, sending directly to their account + /// each venue will have the accrue function called in order to get the most up to + /// date pnl from them + function applyRewards( + address[] calldata venues, + address user + ) external globalLock(1) { + uint256 venueLength = venues.length; + int256 totalVcon = 0; + + unchecked { + for (uint256 i = 0; i < venueLength; i++) { + address venue = venues[i]; + require( + pcvOracle().isVenue(venue), + "MarketGovernance: invalid venue" + ); + + _accrue(venue); + int256 rewards = _harvestRewards(user, venue); + require( + rewards >= 0, + "MarketGovernance: cannot claim rewards on venue with losses" + ); + totalVcon += rewards; + } + } + + /// rewards was never less than 0, so totalVcon must be >= 0 + vcon().safeTransfer(user, totalVcon.toUint256()); + } + + /// @param venues to realize losses in + /// only the caller can realize losses on their own behalf + function realizeLosses(address[] calldata venues) external globalLock(1) { + uint256 venueLength = venues.length; + + unchecked { + for (uint256 i = 0; i < venueLength; i++) { + address venue = venues[i]; + require( + pcvOracle().isVenue(venue), + "MarketGovernance: invalid venue" + ); + + /// updates the venueLastRecordedProfit and venueLastRecordedSharePrice mapping + _accrue(venue); + + /// updates the venueUserStartingVconProfit mapping + int256 losses = _harvestRewards(msg.sender, venue); + require(losses < 0, "MarketGovernance: no losses to realize"); + + if ( + (-losses).toUint256() > + venueUserDepositedVcon[venue][msg.sender] + ) { + uint256 userDepositedAmount = venueUserDepositedVcon[venue][ + msg.sender + ]; + /// zero the user's balance + venueUserDepositedVcon[venue][msg.sender] = 0; + + /// take losses off the total amount staked + vconStaked -= userDepositedAmount; + } else { + /// losses should never exceed staked amount at this point + venueUserDepositedVcon[venue][ + msg.sender + ] = (venueUserDepositedVcon[venue][msg.sender].toInt256() + + losses).toUint256(); + + /// take losses off the total amount staked + vconStaked = (vconStaked.toInt256() + losses).toUint256(); + } + } + } + } + + /// @notice return the amount of rewards accrued so far + /// without calling accrue on the underlying venues + function getAccruedRewards( + address[] calldata venues, + address user + ) external view returns (int256 totalVcon) { + uint256 venueLength = venues.length; + + unchecked { + for (uint256 i = 0; i < venueLength; i++) { + address venue = venues[i]; + require( + pcvOracle().isVenue(venue), + "MarketGovernance: invalid venue" + ); + + totalVcon += getPendingRewards(user, venue); + } + } + } + /// @notice return what the perfectly balanced system would look like function getExpectedPCVAmounts() public @@ -382,22 +490,21 @@ contract MarketGovernance is CoreRefV2, PCVMover { address[] memory pcvDeposits = pcvOracle().getVenues(); uint256 totalVenues = pcvDeposits.length; uint256 totalPcv = pcvOracle().getTotalPcv(); - uint256 cachedTotalSupply = totalSupply; /// Save repeated warm SLOADs + uint256 cachedVconStaked = vconStaked; /// Save repeated warm SLOADs deposits = new PCVDepositInfo[](totalVenues); unchecked { for (uint256 i = 0; i < totalVenues; i++) { address venue = pcvDeposits[i]; - uint256 venueDepositedVcon = totalVenueDepositedVcon[venue]; + uint256 venueDepositedVcon = venueVconDeposited[venue]; deposits[i].deposit = venue; if (venueDepositedVcon == 0) { deposits[i].amount = 0; - continue; } else { uint256 expectedPcvAmount = (venueDepositedVcon * - totalPcv) / cachedTotalSupply; + totalPcv) / cachedVconStaked; deposits[i].amount = expectedPcvAmount; } @@ -496,55 +603,78 @@ contract MarketGovernance is CoreRefV2, PCVMover { ); } + /// @notice VCON rewards could be negative if a user is at a loss + /// @param user to check rewards from + /// @param venue to check rewards in + function getPendingRewards( + address user, + address venue + ) public view returns (int256) { + int256 vconStartIndex = venueUserStartingVconProfit[venue][user] + .toInt256(); + int256 vconCurrentIndex = venueLastRecordedSharePrice[venue].toInt256(); + + if (vconStartIndex == 0) { + return 0; /// no interest if user has not entered the market + } + + int256 vconRewards = (vconCurrentIndex - vconStartIndex) * + venueUserDepositedVcon[venue][user].toInt256(); + + return vconRewards; + } + /// @notice returns profit in VCON a user has accrued /// does not update how much VCON a user has staked to save on gas /// that updating happens in the calling function function _harvestRewards( address user, address venue - ) private returns (uint256) { - uint256 vconStartIndex = startingVenueVconProfit[venue][user]; - uint256 vconCurrentIndex = lastRecordedVconPricePerVenue[venue]; + ) private returns (int256) { + uint256 vconCurrentIndex = venueLastRecordedSharePrice[venue]; - /// do not pay out if user has not entered the market - if (vconStartIndex == 0) { - /// set user starting profit to current venue profit index - startingVenueVconProfit[venue][user] = vconCurrentIndex; - return 0; /// no profits - } + console.log("getting pending rewards"); + /// get pending rewards + int256 pendingRewardBalance = getPendingRewards(user, venue); + console.log("got pending rewards", pendingRewardBalance.toUint256()); - uint256 vconRewards = (vconCurrentIndex - vconStartIndex) * - userVenueDepositedVcon[venue][user]; - startingVenueVconProfit[venue][user] = vconCurrentIndex; + /// then set the vcon current index for this user + venueUserStartingVconProfit[venue][user] = vconCurrentIndex; - return vconRewards; + return pendingRewardBalance; } + /// update the venue last recorded share price function _accrue(address venue) private { - uint256 startingLastRecordedProfit = lastRecordedProfit[venue]; + uint256 startingLastRecordedProfit = venueLastRecordedProfit[venue]; IPCVDepositV2(venue).accrue(); uint256 endingLastRecordedProfit = IPCVDepositV2(venue) .lastRecordedProfit(); - int256 deltaProfit = endingLastRecordedProfit.toInt256() - - startingLastRecordedProfit.toInt256(); /// also could be a loss + + /// update venue last recorded profit regardless + /// of participation in market governance + venueLastRecordedProfit[venue] = endingLastRecordedProfit.toUint128(); /// amount of VCON that each VCON deposit receives for being in this venue /// if there is no supply, there is no delta to apply - if (totalSupply != 0) { + if (vconStaked != 0) { + int256 deltaProfit = endingLastRecordedProfit.toInt256() - + startingLastRecordedProfit.toInt256(); /// also could be a loss + int256 venueProfitToVconRatio = profitToVconRatio[venue].toInt256(); int256 profitPerVconDelta = (deltaProfit * venueProfitToVconRatio) / - totalSupply.toInt256(); + vconStaked.toInt256(); - uint256 lastVconSharePrice = lastRecordedVconPricePerVenue[venue]; + uint256 lastVconSharePrice = venueLastRecordedSharePrice[venue]; require( lastVconSharePrice >= Constants.ETH_GRANULARITY, "MarketGovernance: venue not initialized" ); - lastRecordedVconPricePerVenue[venue] = (lastVconSharePrice + venueLastRecordedSharePrice[venue] = (lastVconSharePrice .toInt256() + profitPerVconDelta).toUint256().toUint128(); } } diff --git a/test/unit/pcv/PCVRouter.t.sol b/test/unit/pcv/PCVRouter.t.sol index b74ebc745..a5161757d 100644 --- a/test/unit/pcv/PCVRouter.t.sol +++ b/test/unit/pcv/PCVRouter.t.sol @@ -420,6 +420,8 @@ contract PCVRouterUnitTest is Test { } function testMovePCVUnsupportedSwap() public { + swapper = new MockPCVSwapper(token1, MockERC20(address(0))); + vm.prank(addresses.governorAddress); address[] memory whitelistedSwapperAddresses = new address[](1); whitelistedSwapperAddresses[0] = address(swapper); diff --git a/test/unit/utils/Deviation.sol b/test/unit/utils/Deviation.sol index dcf68e6c1..bdfd952b3 100644 --- a/test/unit/utils/Deviation.sol +++ b/test/unit/utils/Deviation.sol @@ -60,17 +60,6 @@ library Deviation { return basisPoints; } - /// @notice return the percent deviation between a and b in wei. 1 eth = 100% - function calculateDeviationEthGranularity( - int256 a, - int256 b - ) internal pure returns (int256) { - int256 delta = a - b; - int256 basisPoints = (delta * Constants.ETH_GRANULARITY_INT) / a; - - return basisPoints; - } - /// @notice function to return whether or not the new price is within /// the acceptable deviation threshold function isWithinDeviationThreshold( diff --git a/test/unit/vcon/MarketGovernance.t.sol b/test/unit/vcon/MarketGovernance.t.sol index 33de88be0..0199a72af 100644 --- a/test/unit/vcon/MarketGovernance.t.sol +++ b/test/unit/vcon/MarketGovernance.t.sol @@ -18,6 +18,8 @@ contract UnitTestMarketGovernance is SystemUnitTest { uint256 public usdcDepositAmount = 1_000_000e6; uint256 public vconDepositAmount = 1_000_000e18; + address userOne = address(1000); + address userTwo = address(1001); function setUp() public override { super.setUp(); @@ -79,7 +81,7 @@ contract UnitTestMarketGovernance is SystemUnitTest { vcon.mint(address(this), vconDepositAmount); vcon.approve(address(mgov), vconDepositAmount); - mgov.deposit( + mgov.stake( vconDepositAmount, pcvDepositDai.balance(), address(pcvDepositDai), @@ -91,6 +93,13 @@ contract UnitTestMarketGovernance is SystemUnitTest { pcvOracle.getTotalPcv(), pcvOracle.getVenueBalance(address(pcvDepositUsdc)) ); + + assertEq( + mgov.venueVconDeposited(address(pcvDepositUsdc)), + vconDepositAmount + ); + + assertEq(mgov.vconStaked(), vconDepositAmount); } function testSystemTwoUsers() public { @@ -100,13 +109,11 @@ contract UnitTestMarketGovernance is SystemUnitTest { totalPCV = pcvOracle.getTotalPcv(); - address user = address(1000); - - vm.startPrank(user); + vm.startPrank(userOne); - vcon.mint(user, vconDepositAmount); + vcon.mint(userOne, vconDepositAmount); vcon.approve(address(mgov), vconDepositAmount); - mgov.deposit( + mgov.stake( vconDepositAmount, pcvDepositUsdc.balance() / 2, address(pcvDepositUsdc), @@ -116,7 +123,7 @@ contract UnitTestMarketGovernance is SystemUnitTest { vm.stopPrank(); - assertEq(vcon.balanceOf(user), 0); + assertEq(vcon.balanceOf(userOne), 0); assertEq( pcvOracle.getTotalPcv() / 2, pcvOracle.getVenueBalance(address(pcvDepositUsdc)) @@ -131,21 +138,26 @@ contract UnitTestMarketGovernance is SystemUnitTest { vm.assume(vconAmount > 1e9); testSystemTwoUsers(); - address user = address(1001); - - uint256 startingTotalSupply = mgov.totalSupply(); + uint256 startingTotalSupply = mgov.vconStaked(); - vm.startPrank(user); + vm.startPrank(userTwo); - vcon.mint(user, vconAmount); + vcon.mint(userTwo, vconAmount); vcon.approve(address(mgov), vconAmount); - mgov.depositNoMove(address(pcvDepositUsdc), vconAmount); + mgov.stake( + vconAmount, + 0, /// deposit no pcv, meaning things will be imbalanced + address(pcvDepositDai), + address(pcvDepositUsdc), + address(pcvSwapper) + ); vm.stopPrank(); - assertEq(vcon.balanceOf(user), 0); + assertEq(vcon.balanceOf(userTwo), 0); + assertEq(mgov.venueVconDeposited(userTwo), vconAmount); - uint256 endingTotalSupply = mgov.totalSupply(); + uint256 endingTotalSupply = mgov.vconStaked(); uint256 totalPCV = pcvOracle.getTotalPcv(); assertEq(endingTotalSupply, startingTotalSupply + vconAmount); @@ -158,15 +170,21 @@ contract UnitTestMarketGovernance is SystemUnitTest { uint120 vconAmount = 1e9; address user = address(1001); - uint256 startingTotalSupply = mgov.totalSupply(); + uint256 startingTotalSupply = mgov.vconStaked(); vm.startPrank(user); vcon.mint(user, vconAmount); vcon.approve(address(mgov), vconAmount); - mgov.depositNoMove(address(pcvDepositUsdc), vconAmount); + mgov.stake( + vconAmount, + 0, /// deposit no pcv, meaning things will be imbalanced + address(pcvDepositDai), + address(pcvDepositUsdc), + address(pcvSwapper) + ); vm.stopPrank(); - uint256 endingTotalSupply = mgov.totalSupply(); + uint256 endingTotalSupply = mgov.vconStaked(); uint256 totalPCV = pcvOracle.getTotalPcv(); assertEq(vcon.balanceOf(user), 0); @@ -181,15 +199,21 @@ contract UnitTestMarketGovernance is SystemUnitTest { _initializeVenues(); address user = address(1001); - uint256 startingTotalSupply = mgov.totalSupply(); + uint256 startingTotalSupply = mgov.vconStaked(); vm.startPrank(user); vcon.mint(user, vconAmount); vcon.approve(address(mgov), vconAmount); - mgov.depositNoMove(address(pcvDepositUsdc), vconAmount); + mgov.stake( + vconAmount, + 0, /// deposit no pcv, meaning things will be imbalanced + address(pcvDepositDai), + address(pcvDepositUsdc), + address(pcvSwapper) + ); vm.stopPrank(); - uint256 endingTotalSupply = mgov.totalSupply(); + uint256 endingTotalSupply = mgov.vconStaked(); uint256 totalPCV = pcvOracle.getTotalPcv(); assertEq(vcon.balanceOf(user), 0); @@ -207,11 +231,17 @@ contract UnitTestMarketGovernance is SystemUnitTest { vm.startPrank(user); vcon.mint(user, vconAmount); vcon.approve(address(mgov), vconAmount); - mgov.depositNoMove(address(pcvDepositUsdc), vconAmount); + mgov.stake( + vconAmount, + 0, + address(pcvDepositDai), + address(pcvDepositUsdc), + address(pcvSwapper) + ); vm.stopPrank(); vm.expectRevert("MarketGovernance: venue not initialized"); - mgov.deposit( + mgov.stake( vconAmount, 1e18, address(pcvDepositUsdc), @@ -220,7 +250,13 @@ contract UnitTestMarketGovernance is SystemUnitTest { ); vm.expectRevert("MarketGovernance: venue not initialized"); - mgov.depositNoMove(address(pcvDepositDai), vconAmount); + mgov.stake( + vconAmount, + 0, /// deposit no pcv, meaning things will be imbalanced + address(pcvDepositUsdc), + address(pcvDepositDai), + address(pcvSwapper) + ); } function testReinitializingVenueFails() public { @@ -230,6 +266,100 @@ contract UnitTestMarketGovernance is SystemUnitTest { mgov.initializeVenue(address(pcvDepositUsdc)); } + function testUnstakingOneUser() public { + testSystemOneUser(); + + assertEq(vcon.balanceOf(address(this)), 0); + uint256 vconAmount = mgov.vconStaked(); + + mgov.unstake( + vconAmount, + address(pcvDepositUsdc), + address(pcvDepositDai), + address(pcvSwapper), + address(this) + ); + + assertEq(vcon.balanceOf(address(this)), vconAmount); + + /// all funds moved to DAI PCV deposit + assertEq( + pcvOracle.getTotalPcv(), + pcvOracle.getVenueBalance(address(pcvDepositDai)) + ); + assertEq(0, mgov.venueVconDeposited(address(pcvDepositDai))); + + assertEq(0, pcvOracle.getVenueBalance(address(pcvDepositUsdc))); + assertEq(0, mgov.venueVconDeposited(address(pcvDepositUsdc))); + + assertEq(0, mgov.vconStaked()); + } + + function testUnstakingTwoUsers() public { + testSystemTwoUsers(); + + assertEq(vcon.balanceOf(address(this)), 0); + + uint256 startingVconStakedAmount = mgov.vconStaked(); + uint256 vconAmount = mgov.venueUserDepositedVcon( + address(pcvDepositUsdc), + address(this) + ); + + mgov.unstake( + vconAmount, + address(pcvDepositUsdc), + address(pcvDepositDai), + address(pcvSwapper), + address(this) + ); + + assertEq(vcon.balanceOf(address(this)), vconAmount); + + /// all funds moved to DAI PCV deposit + assertEq( + pcvOracle.getTotalPcv(), + pcvOracle.getVenueBalance(address(pcvDepositDai)) + ); + assertEq( + vconAmount - startingVconStakedAmount, + mgov.venueVconDeposited(address(pcvDepositDai)) + ); + + assertEq(0, pcvOracle.getVenueBalance(address(pcvDepositUsdc))); + assertEq(0, mgov.venueVconDeposited(address(pcvDepositUsdc))); + + assertEq(vconAmount - startingVconStakedAmount, mgov.vconStaked()); + } + + /// test withdrawing when src and dest are equal + function testWithdrawFailsSrcDestEqual() public { + uint256 vconAmount = mgov.vconStaked(); + + vm.expectRevert("MarketGovernance: src and dest equal"); + mgov.unstake( + vconAmount, + address(pcvDepositUsdc), + address(pcvDepositUsdc), + address(0), + address(this) + ); + } + + /// test depositing when src and dest are equal + function testDepositFailsSrcDestEqual() public { + uint256 vconAmount = mgov.vconStaked(); + + vm.expectRevert("MarketGovernance: src and dest equal"); + mgov.stake( + vconAmount, + 0, + address(pcvDepositUsdc), + address(pcvDepositUsdc), + address(0) + ); + } + /// todo test withdrawing /// todo test depositing fails when venue not initialized /// todo test withdrawing when there are profits From e37a9071b07a7fd1a29baea3f83e1740a1ebbc21 Mon Sep 17 00:00:00 2001 From: Elliot Date: Wed, 25 Jan 2023 22:40:00 -0800 Subject: [PATCH 29/74] remove PCVMover, roll back PCVRouter --- src/pcv/IPCVMover.sol | 29 -------- src/pcv/IPCVRouter.sol | 36 ++++++++++ src/pcv/PCVMover.sol | 140 ------------------------------------ src/pcv/PCVRouter.sol | 158 ++++++++++++++++++++++++++++++++++++++++- 4 files changed, 192 insertions(+), 171 deletions(-) delete mode 100644 src/pcv/IPCVMover.sol delete mode 100644 src/pcv/PCVMover.sol diff --git a/src/pcv/IPCVMover.sol b/src/pcv/IPCVMover.sol deleted file mode 100644 index 0fcbfa0c7..000000000 --- a/src/pcv/IPCVMover.sol +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.13; - -interface IPCVMover { - // ----------- Events ----------- - - event PCVMovement( - address indexed source, - address indexed destination, - uint256 amountSource, - uint256 amountDestination - ); - - event PCVSwapperAdded(address indexed swapper); - - event PCVSwapperRemoved(address indexed swapper); - - // ---------- Read-Only API ------------------ - - function isPCVSwapper(address pcvSwapper) external returns (bool); - - function getPCVSwappers() external returns (address[] memory); - - // ---------- PCVSwapper Management API ------ - - function addPCVSwappers(address[] calldata _pcvSwappers) external; - - function removePCVSwappers(address[] calldata _pcvSwappers) external; -} diff --git a/src/pcv/IPCVRouter.sol b/src/pcv/IPCVRouter.sol index c41774346..d4712f9b9 100644 --- a/src/pcv/IPCVRouter.sol +++ b/src/pcv/IPCVRouter.sol @@ -4,6 +4,42 @@ pragma solidity 0.8.13; /// @title a PCV Router interface /// @author Volt Protocol interface IPCVRouter { + // ----------- Events ----------- + + event PCVMovement( + address indexed source, + address indexed destination, + uint256 amountSource, + uint256 amountDestination + ); + + event PCVSwapperAdded(address indexed swapper); + + event PCVSwapperRemoved(address indexed swapper); + + // ---------- Read-Only API ------------------ + + function isPCVSwapper(address pcvSwapper) external returns (bool); + + function getPCVSwappers() external returns (address[] memory); + + // ---------- PCVSwapper Management API ------ + + function addPCVSwappers(address[] calldata _pcvSwappers) external; + + function removePCVSwappers(address[] calldata _pcvSwappers) external; + + // ----------- PCV_MOVER role API ----------- + + function movePCV( + address source, + address destination, + address swapper, + uint256 amount, + address sourceAsset, + address destinationAsset + ) external; + // ----------- PCV_CONTROLLER role api ----------- function movePCVUnchecked( diff --git a/src/pcv/PCVMover.sol b/src/pcv/PCVMover.sol deleted file mode 100644 index ab78f0c0b..000000000 --- a/src/pcv/PCVMover.sol +++ /dev/null @@ -1,140 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.13; - -import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; - -import {console} from "@forge-std/console.sol"; -import {IPCVMover} from "@voltprotocol/pcv/IPCVMover.sol"; -import {CoreRefV2} from "@voltprotocol/refs/CoreRefV2.sol"; -import {IPCVOracle} from "@voltprotocol/oracle/IPCVOracle.sol"; -import {IPCVSwapper} from "@voltprotocol/pcv/IPCVSwapper.sol"; -import {IPCVDeposit} from "@voltprotocol/pcv/IPCVDeposit.sol"; - -abstract contract PCVMover is IPCVMover, CoreRefV2 { - using EnumerableSet for EnumerableSet.AddressSet; - - ///@notice set of whitelisted PCV swappers - EnumerableSet.AddressSet private pcvSwappers; - - // ---------- Read-Only API ------------------ - - /// @notice returns true if the the provided address is a valid swapper to use - /// @param pcvSwapper the pcvSwapper address to check if whitelisted - function isPCVSwapper( - address pcvSwapper - ) public view override returns (bool) { - return pcvSwappers.contains(pcvSwapper); - } - - /// @notice returns all whitelisted PCV swappers - function getPCVSwappers() - external - view - override - returns (address[] memory) - { - return pcvSwappers.values(); - } - - // ---------- PCVSwapper Management API ------ - - /// @notice Add multiple PCV Swappers to the whitelist - /// @param _pcvSwappers the addresses to whitelist, as calldata - function addPCVSwappers( - address[] calldata _pcvSwappers - ) external override onlyGovernor { - unchecked { - for (uint256 i = 0; i < _pcvSwappers.length; i++) { - require( - pcvSwappers.add(_pcvSwappers[i]), - "PCVRouter: Failed to add swapper" - ); - emit PCVSwapperAdded(_pcvSwappers[i]); - } - } - } - - /// @notice Remove multiple PCV Swappers from the whitelist - /// @param _pcvSwappers the addresses to remove from whitelist, as calldata - function removePCVSwappers( - address[] calldata _pcvSwappers - ) external override onlyGovernor { - unchecked { - for (uint256 i = 0; i < _pcvSwappers.length; i++) { - require( - pcvSwappers.remove(_pcvSwappers[i]), - "PCVRouter: Failed to remove swapper" - ); - emit PCVSwapperRemoved(_pcvSwappers[i]); - } - } - } - - /// ------------- Helper Methods ------------- - function _movePCV( - address source, - address destination, - address swapper, - uint256 amountSource, - address sourceAsset, - address destinationAsset - ) internal { - // Do transfer - uint256 amountDestination; - if (swapper != address(0)) { - IPCVDeposit(source).withdraw(swapper, amountSource); - amountDestination = IPCVSwapper(swapper).swap( - sourceAsset, - destinationAsset, - destination - ); - console.log("Successfully swapped: ", amountDestination); - } else { - IPCVDeposit(source).withdraw(destination, amountSource); - amountDestination = amountSource; - } - IPCVDeposit(destination).deposit(); - - // Emit event - emit PCVMovement(source, destination, amountSource, amountDestination); - } - - function _checkPCVMove( - address source, - address destination, - address swapper, - address sourceAsset, - address destinationAsset - ) internal view { - // Check both deposits are still valid for PCVOracle - IPCVOracle _pcvOracle = pcvOracle(); - require(_pcvOracle.isVenue(source), "PCVRouter: invalid source"); - require( - _pcvOracle.isVenue(destination), - "PCVRouter: invalid destination" - ); - - // Check underlying tokens - require( - IPCVDeposit(source).balanceReportedIn() == sourceAsset, - "PCVRouter: invalid source asset" - ); - require( - IPCVDeposit(destination).balanceReportedIn() == destinationAsset, - "PCVRouter: invalid destination asset" - ); - - if (sourceAsset != destinationAsset) { - require(swapper != address(0), "MarketGovernance: invalid swapper"); - } - - // Check swapper, if applicable - if (swapper != address(0)) { - require(isPCVSwapper(swapper), "PCVRouter: invalid swapper"); - require( - IPCVSwapper(swapper).canSwap(sourceAsset, destinationAsset), - "PCVRouter: unsupported swap" - ); - } - } -} diff --git a/src/pcv/PCVRouter.sol b/src/pcv/PCVRouter.sol index 9b0fec51f..2770e80a7 100644 --- a/src/pcv/PCVRouter.sol +++ b/src/pcv/PCVRouter.sol @@ -1,7 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.13; -import {PCVMover} from "@voltprotocol/pcv/PCVMover.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + import {CoreRefV2} from "@voltprotocol/refs/CoreRefV2.sol"; import {VoltRoles} from "@voltprotocol/core/VoltRoles.sol"; import {IPCVOracle} from "@voltprotocol/oracle/IPCVOracle.sol"; @@ -13,11 +14,136 @@ import {IPCVDeposit} from "@voltprotocol/pcv/IPCVDeposit.sol"; /// @notice A contract that allows PCV movements between deposits. /// @dev This contract requires the PCV_CONTROLLER role. /// @author eswak -contract PCVRouter is IPCVRouter, CoreRefV2, PCVMover { +contract PCVRouter is IPCVRouter, CoreRefV2 { + using EnumerableSet for EnumerableSet.AddressSet; + + ///@notice set of whitelisted PCV swappers + EnumerableSet.AddressSet private pcvSwappers; + constructor(address _core) CoreRefV2(_core) {} + // ---------- Read-Only API ------------------ + + /// @notice returns true if the the provided address is a valid swapper to use + /// @param pcvSwapper the pcvSwapper address to check if whitelisted + function isPCVSwapper( + address pcvSwapper + ) public view override returns (bool) { + return pcvSwappers.contains(pcvSwapper); + } + + /// @notice returns all whitelisted PCV swappers + function getPCVSwappers() + external + view + override + returns (address[] memory) + { + return pcvSwappers.values(); + } + + // ---------- PCVSwapper Management API ------ + + /// @notice Add multiple PCV Swappers to the whitelist + /// @param _pcvSwappers the addresses to whitelist, as calldata + function addPCVSwappers( + address[] calldata _pcvSwappers + ) external override onlyGovernor { + unchecked { + for (uint256 i = 0; i < _pcvSwappers.length; i++) { + require( + pcvSwappers.add(_pcvSwappers[i]), + "PCVRouter: Failed to add swapper" + ); + emit PCVSwapperAdded(_pcvSwappers[i]); + } + } + } + + /// @notice Remove multiple PCV Swappers from the whitelist + /// @param _pcvSwappers the addresses to remove from whitelist, as calldata + function removePCVSwappers( + address[] calldata _pcvSwappers + ) external override onlyGovernor { + unchecked { + for (uint256 i = 0; i < _pcvSwappers.length; i++) { + require( + pcvSwappers.remove(_pcvSwappers[i]), + "PCVRouter: Failed to remove swapper" + ); + emit PCVSwapperRemoved(_pcvSwappers[i]); + } + } + } + // ---------- PCV Movement API --------------- + /// @notice Move PCV by withdrawing it from a PCVDeposit and deposit it in + /// a destination PCVDeposit, eventually using a PCVSwapper in-between + /// for asset conversion. + /// + /// Only callable at lock level 1. + /// + /// This function requires a less trusted PCV_MOVER role, and performs checks + /// at runtime that the PCV Deposits are indeed added in the PCV Oracle, that + /// underlying tokens are correct, and that the PCVSwapper used (if any) has + /// previously been whitelisted through governance. + /// @param source the address of the pcv deposit contract to withdraw from + /// @param destination the address of the pcv deposit contract to deposit into + /// @param swapper the PCVSwapper to use for asset conversion, address(0) for no conversion. + /// @param amount the amount to withdraw and deposit + /// @param sourceAsset the token address of the source PCV Deposit + /// @param destinationAsset the token address of the destination PCV Deposit + function movePCV( + address source, + address destination, + address swapper, + uint256 amount, + address sourceAsset, + address destinationAsset + ) + external + whenNotPaused + onlyVoltRole(VoltRoles.PCV_MOVER) + isGlobalReentrancyLocked(1) + { + // Check both deposits are still valid for PCVOracle + IPCVOracle _pcvOracle = pcvOracle(); + require(_pcvOracle.isVenue(source), "PCVRouter: invalid source"); + require( + _pcvOracle.isVenue(destination), + "PCVRouter: invalid destination" + ); + + // Check underlying tokens + require( + IPCVDeposit(source).balanceReportedIn() == sourceAsset, + "PCVRouter: invalid source asset" + ); + require( + IPCVDeposit(destination).balanceReportedIn() == destinationAsset, + "PCVRouter: invalid destination asset" + ); + // Check swapper, if applicable + if (swapper != address(0)) { + require(isPCVSwapper(swapper), "PCVRouter: invalid swapper"); + require( + IPCVSwapper(swapper).canSwap(sourceAsset, destinationAsset), + "PCVRouter: unsupported swap" + ); + } + + // Do movement + _movePCV( + source, + destination, + swapper, + amount, + sourceAsset, + destinationAsset + ); + } + /// @notice Move PCV by withdrawing it from a PCVDeposit and deposit it in /// a destination PCVDeposit. /// This function requires the highly trusted PCV_CONTROLLER role, and expects @@ -74,4 +200,32 @@ contract PCVRouter is IPCVRouter, CoreRefV2, PCVMover { destinationAsset ); } + + /// ------------- Helper Methods ------------- + function _movePCV( + address source, + address destination, + address swapper, + uint256 amountSource, + address sourceAsset, + address destinationAsset + ) internal { + // Do transfer + uint256 amountDestination; + if (swapper != address(0)) { + IPCVDeposit(source).withdraw(swapper, amountSource); + amountDestination = IPCVSwapper(swapper).swap( + sourceAsset, + destinationAsset, + destination + ); + } else { + IPCVDeposit(source).withdraw(destination, amountSource); + amountDestination = amountSource; + } + IPCVDeposit(destination).deposit(); + + // Emit event + emit PCVMovement(source, destination, amountSource, amountDestination); + } } From 4bb9d8bec741ad4e4d805d90f9c0131fa6196837 Mon Sep 17 00:00:00 2001 From: Elliot Date: Wed, 25 Jan 2023 22:40:47 -0800 Subject: [PATCH 30/74] PCV Router Architecture --- src/vcon/IMarketGovernance.sol | 31 ++-- src/vcon/MarketGovernance.sol | 316 +++++++++++++-------------------- 2 files changed, 129 insertions(+), 218 deletions(-) diff --git a/src/vcon/IMarketGovernance.sol b/src/vcon/IMarketGovernance.sol index 078b84c64..3b8c5eb0b 100644 --- a/src/vcon/IMarketGovernance.sol +++ b/src/vcon/IMarketGovernance.sol @@ -32,19 +32,10 @@ interface IMarketGovernance { /// ---------- Permissionless User PCV Allocation Methods ---------- - /// this function can be called with amount of PCV set to 0 + /// stake VCON on a venue /// @param amountVcon to stake - /// @param amountPcv to move from src to dest /// @param source pcv deposit to pull funds from - /// @param destination pcv deposit to send funds - /// @param swapper address to swap tokens - function stake( - uint256 amountVcon, - uint256 amountPcv, - address source, - address destination, - address swapper - ) external; + function stake(uint256 amountVcon, address source) external; /// @notice this function automatically calculates /// the amount of PCV to remove from the source @@ -69,10 +60,14 @@ interface IMarketGovernance { /// @param movements of PCV between venues function rebalance(Rebalance[] calldata movements) external; - /// apply the amount of rewards a user has accrued, sending directly to their account - /// each venue will have the accrue function called in order to get the most up to - /// date pnl from them - function applyRewards(address[] calldata venues, address user) external; + /// @notice realize gains and losses for msg.sender + /// @param venues to realize losses in + /// only the caller can realize losses on their own behalf + /// duplicating addresses does not allow theft as all venues have their indexes + /// updated before we find the profit and loss, so a duplicate venue will have 0 delta a second time + /// @dev we can't follow CEI here because we have to make external calls to update + /// the external venues. However, this is not an issue as the global reentrancy lock is enabled + function realizeGainsAndLosses(address[] calldata venues) external; /// return the total amount of rewards a user is entitled to /// this value will usually be stale as .accrue() must be called in the same block/tx as this function @@ -81,10 +76,4 @@ interface IMarketGovernance { address[] calldata venues, address user ) external view returns (int256); - - /// ---------- Initialize Method ---------- - - /// @notice permissionlessly initialize a venue - /// required to be able to utilize a given PCV Deposit in market governance - function initializeVenue(address venue) external; } diff --git a/src/vcon/MarketGovernance.sol b/src/vcon/MarketGovernance.sol index c9c443583..ac9ca93a2 100644 --- a/src/vcon/MarketGovernance.sol +++ b/src/vcon/MarketGovernance.sol @@ -4,8 +4,8 @@ pragma solidity =0.8.13; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; -import {PCVMover} from "@voltprotocol/pcv/PCVMover.sol"; import {Constants} from "@voltprotocol/Constants.sol"; +import {PCVRouter} from "@voltprotocol/pcv/PCVRouter.sol"; import {CoreRefV2} from "@voltprotocol/refs/CoreRefV2.sol"; import {IPCVOracle} from "@voltprotocol/oracle/IPCVOracle.sol"; import {IPCVDeposit} from "@voltprotocol/pcv/IPCVDeposit.sol"; @@ -15,7 +15,7 @@ import {DeviationWeiGranularity} from "@voltprotocol/utils/DeviationWeiGranulari import {console} from "@forge-std/console.sol"; -/// @notice this contract requires the PCV Controller and Locker role +/// @notice this contract requires the PCV Mover and Locker role /// /// Core formula for market governance rewards: /// Profit Per VCON = Profit Per VCON + ∆Cumulative Profits (Dollars) * VCON:Dollar / VCON Staked @@ -27,16 +27,15 @@ import {console} from "@forge-std/console.sol"; /// The VCON:Dollar ratio is the same for both profits and losses. If a venue has a VCON:Dollar ratio of 5:1 /// and the venue gains $5 in profits, then 25 VCON will be distributed across all VCON stakers in that venue. /// If that same venue losses $5, then a loss of 25 VCON will be distributed across all VCON stakers in that venue. -contract MarketGovernance is CoreRefV2, PCVMover, IMarketGovernance { +contract MarketGovernance is CoreRefV2, IMarketGovernance { using DeviationWeiGranularity for *; using SafeERC20 for *; using SafeCast for *; - /// @notice emitted when a route is added - event RouteAdded( - address indexed src, - address indexed dst, - address indexed swapper + /// @notice emitted when the router is updated + event PCVRouterUpdated( + address indexed oldPcvRouter, + address indexed newPcvRouter ); /// @notice emitted when profit to vcon ratio is updated @@ -46,6 +45,9 @@ contract MarketGovernance is CoreRefV2, PCVMover, IMarketGovernance { uint256 newRatio ); + /// @notice reference to the PCV Router + address public pcvRouter; + /// @notice total amount of VCON deposited across all venues uint256 public vconStaked; @@ -56,18 +58,13 @@ contract MarketGovernance is CoreRefV2, PCVMover, IMarketGovernance { /// which will not be included in the V1 mapping(address => uint256) public profitToVconRatio; - /// TODO simplify this contract down to only track venue profit, /// and do the conversion to VCON at the end when accruing rewards - /// alternatively, pack venueLastRecordedSharePrice, - /// venueLastRecordedProfit and venueVconDeposited into a single slot for gas optimization + /// pack venueLastRecordedProfit, venueVconDeposited and profitToVconRatio + /// into a single slot for gas optimization /// @notice last recorded profit index per venue mapping(address => uint128) public venueLastRecordedProfit; - /// @notice last recorded share price of a venue in VCON - /// starts off at 1e18 - mapping(address => uint128) public venueLastRecordedSharePrice; - /// @notice total vcon deposited per venue mapping(address => uint256) public venueVconDeposited; @@ -77,39 +74,16 @@ contract MarketGovernance is CoreRefV2, PCVMover, IMarketGovernance { /// @notice record of VCON index when user joined a given venue mapping(address => mapping(address => uint256)) - public venueUserStartingVconProfit; + public venueUserStartingProfit; /// @notice record how much VCON a user deposited in a given venue mapping(address => mapping(address => uint256)) public venueUserDepositedVcon; /// @param _core reference to core - constructor(address _core) CoreRefV2(_core) {} - - /// @notice permissionlessly initialize a venue - /// required to be able to utilize a given PCV Deposit in market governance - function initializeVenue(address venue) external globalLock(1) { - require(pcvOracle().isVenue(venue), "MarketGovernance: invalid venue"); - - uint256 startingLastRecordedProfit = venueLastRecordedProfit[venue]; - require( - startingLastRecordedProfit == 0, - "MarketGovernance: profit already recorded" - ); - require( - venueLastRecordedSharePrice[venue] == 0, - "MarketGovernance: venue already has share price" - ); - - IPCVDepositV2(venue).accrue(); - - uint256 endingLastRecordedProfit = IPCVDepositV2(venue) - .lastRecordedProfit(); - - venueLastRecordedProfit[venue] = endingLastRecordedProfit.toUint128(); - venueLastRecordedSharePrice[venue] = Constants - .ETH_GRANULARITY - .toUint128(); + /// @param _pcvRouter reference to pcvRouter + constructor(address _core, address _pcvRouter) CoreRefV2(_core) { + pcvRouter = _pcvRouter; } /// TODO update pcv oracle to cache total pcv so getAllPCV isn't needed to figure @@ -117,32 +91,20 @@ contract MarketGovernance is CoreRefV2, PCVMover, IMarketGovernance { /// ---------- Permissionless User PCV Allocation Methods ---------- - /// any losses or gains are applied to venueLastRecordedSharePrice - /// - /// - /// user deposits - /// @notice a user can get slashed up to their full VCON stake for entering /// a venue that takes a loss. + /// any losses or gains are applied to venueUserStartingProfit via `_accrue` method /// @param amountVcon to stake on destination - /// @param amountPcv to move from source to destination - /// @param source address to pull funds from /// @param destination address to accrue rewards to, and send funds to - /// @param swapper address that swaps asset types between src and dest if needed function stake( uint256 amountVcon, - uint256 amountPcv, - address source, - address destination, - address swapper + address destination ) external globalLock(1) { IPCVOracle oracle = pcvOracle(); - require(oracle.isVenue(source), "MarketGovernance: invalid source"); require( oracle.isVenue(destination), "MarketGovernance: invalid destination" ); - require(source != destination, "MarketGovernance: src and dest equal"); _accrue(destination); /// update profitPerVCON in the destination so the user gets in at the current share price int256 vconRewards = _harvestRewards(msg.sender, destination); /// auto-compound rewards @@ -151,9 +113,6 @@ contract MarketGovernance is CoreRefV2, PCVMover, IMarketGovernance { "MarketGovernance: must realize loss before staking" ); - /// check and an interaction with a trusted contract - vcon().safeTransferFrom(msg.sender, address(this), amountVcon); /// transfer VCON in - uint256 totalVconDeposited = amountVcon + vconRewards.toUint256(); /// auto-compound rewards /// global updates @@ -165,18 +124,8 @@ contract MarketGovernance is CoreRefV2, PCVMover, IMarketGovernance { /// venue updates venueVconDeposited[destination] += totalVconDeposited; - /// ignore balance checks if only one user is allocating in the system - bool ignoreBalanceChecks = vconStaked == totalVconDeposited; - - if (amountPcv != 0) { - _movePCVWithChecks( - source, - destination, - swapper, - amountPcv, - ignoreBalanceChecks - ); - } + /// check and an interaction with a trusted contract + vcon().safeTransferFrom(msg.sender, address(this), amountVcon); /// transfer VCON in } /// @notice unstake VCON and transfer corresponding VCON to another venue @@ -219,8 +168,7 @@ contract MarketGovernance is CoreRefV2, PCVMover, IMarketGovernance { ); require( - venueUserDepositedVcon[source][msg.sender] + vconReward >= - amountVcon, + venueUserDepositedVcon[source][msg.sender] >= amountVcon, "MarketGovernance: invalid vcon amount" ); @@ -228,20 +176,14 @@ contract MarketGovernance is CoreRefV2, PCVMover, IMarketGovernance { vconStaked -= amountVcon; /// user updates - /// balance = 80 - /// amount = 100 - /// rewards = 30 - /// amount deducted = 70 - /// _____________ - /// balance = 10 - venueUserDepositedVcon[source][msg.sender] -= amountVcon - vconReward; + venueUserDepositedVcon[source][msg.sender] -= amountVcon; /// venue updates venueVconDeposited[source] -= amountVcon; /// ---------- Interactions ---------- - vcon().safeTransfer(vconRecipient, amountVcon); /// transfer VCON to recipient + vcon().safeTransfer(vconRecipient, amountVcon + vconReward); /// transfer VCON amount + rewards to recipient /// ignore balance checks if only one user is in the system and is allocating to a single venue bool ignoreBalanceChecks = vconStaked == @@ -302,7 +244,7 @@ contract MarketGovernance is CoreRefV2, PCVMover, IMarketGovernance { /// /// @param venue to query /// @param totalPcv to measure venue against - function getVenueBalance( + function getVenueDeviation( address venue, uint256 totalPcv ) public view returns (int256) { @@ -332,11 +274,7 @@ contract MarketGovernance is CoreRefV2, PCVMover, IMarketGovernance { if (venueDepositedVconRatio == 0) { /// add this 0 check because deviation divides by a and would cause a revert /// replicate step 3 if no VCON is deposited by comparing pcv to venue balance - return - DeviationWeiGranularity.calculateDeviation( - totalPcv.toInt256(), - venueBalance.toInt256() - ); + return Constants.ETH_GRANULARITY_INT; } /// Step 2. @@ -377,47 +315,18 @@ contract MarketGovernance is CoreRefV2, PCVMover, IMarketGovernance { return proRataPcv; } - struct PCVDepositInfo { - address deposit; - uint256 amount; - } - - /// apply the amount of rewards a user has accrued, sending directly to their account - /// each venue will have the accrue function called in order to get the most up to - /// date pnl from them - function applyRewards( - address[] calldata venues, - address user - ) external globalLock(1) { - uint256 venueLength = venues.length; - int256 totalVcon = 0; - - unchecked { - for (uint256 i = 0; i < venueLength; i++) { - address venue = venues[i]; - require( - pcvOracle().isVenue(venue), - "MarketGovernance: invalid venue" - ); - - _accrue(venue); - int256 rewards = _harvestRewards(user, venue); - require( - rewards >= 0, - "MarketGovernance: cannot claim rewards on venue with losses" - ); - totalVcon += rewards; - } - } - - /// rewards was never less than 0, so totalVcon must be >= 0 - vcon().safeTransfer(user, totalVcon.toUint256()); - } - + /// @notice realize gains and losses for msg.sender /// @param venues to realize losses in /// only the caller can realize losses on their own behalf - function realizeLosses(address[] calldata venues) external globalLock(1) { + /// duplicating addresses does not allow theft as all venues have their indexes + /// updated before we find the profit and loss, so a duplicate venue will have 0 delta a second time + /// @dev we can't follow CEI here because we have to make external calls to update + /// the external venues. However, this is not an issue as the global reentrancy lock is enabled + function realizeGainsAndLosses( + address[] calldata venues + ) external globalLock(1) { uint256 venueLength = venues.length; + int256 totalVcon = 0; unchecked { for (uint256 i = 0; i < venueLength; i++) { @@ -427,37 +336,53 @@ contract MarketGovernance is CoreRefV2, PCVMover, IMarketGovernance { "MarketGovernance: invalid venue" ); - /// updates the venueLastRecordedProfit and venueLastRecordedSharePrice mapping + /// updates the venueLastRecordedProfit _accrue(venue); - /// updates the venueUserStartingVconProfit mapping - int256 losses = _harvestRewards(msg.sender, venue); - require(losses < 0, "MarketGovernance: no losses to realize"); - - if ( - (-losses).toUint256() > - venueUserDepositedVcon[venue][msg.sender] - ) { - uint256 userDepositedAmount = venueUserDepositedVcon[venue][ - msg.sender - ]; - /// zero the user's balance - venueUserDepositedVcon[venue][msg.sender] = 0; - - /// take losses off the total amount staked - vconStaked -= userDepositedAmount; + /// updates the venueUserStartingProfit mapping + int256 pnl = _harvestRewards(msg.sender, venue); + if (pnl < 0) { + /// loss scenarios + if ( + (-pnl).toUint256() > + venueUserDepositedVcon[venue][msg.sender] + ) { + uint256 userDepositedAmount = venueUserDepositedVcon[ + venue + ][msg.sender]; + /// zero the user's balance + venueUserDepositedVcon[venue][msg.sender] = 0; + + /// take losses off the total amount staked + vconStaked -= userDepositedAmount; + + /// TODO final write to storage, decrement venue deposited VCON + venueVconDeposited[venue] -= userDepositedAmount; + } else { + /// losses should never exceed staked amount at this point + venueUserDepositedVcon[venue][ + msg.sender + ] = (venueUserDepositedVcon[venue][msg.sender] + .toInt256() + pnl).toUint256(); + + /// take losses off the total amount staked + vconStaked = (vconStaked.toInt256() + pnl).toUint256(); + + /// TODO final write to storage, decrement venue deposited VCON + venueVconDeposited[venue] -= (-pnl).toUint256(); + } } else { - /// losses should never exceed staked amount at this point - venueUserDepositedVcon[venue][ - msg.sender - ] = (venueUserDepositedVcon[venue][msg.sender].toInt256() + - losses).toUint256(); - - /// take losses off the total amount staked - vconStaked = (vconStaked.toInt256() + losses).toUint256(); + /// gain or even scenario + totalVcon += pnl; } + + /// TODO emit an event } } + + if (totalVcon != 0) { + vcon().safeTransfer(msg.sender, totalVcon.toUint256()); + } } /// @notice return the amount of rewards accrued so far @@ -481,6 +406,11 @@ contract MarketGovernance is CoreRefV2, PCVMover, IMarketGovernance { } } + struct PCVDepositInfo { + address deposit; + uint256 amount; + } + /// @notice return what the perfectly balanced system would look like function getExpectedPCVAmounts() public @@ -538,22 +468,13 @@ contract MarketGovernance is CoreRefV2, PCVMover, IMarketGovernance { if (!ignoreBalanceChecks) { /// record how balanced the system is before the PCV movement - sourceVenueBalance = getVenueBalance(source, totalPcv); /// 50% - destinationVenueBalance = getVenueBalance(destination, totalPcv); /// -30% + sourceVenueBalance = getVenueDeviation(source, totalPcv); /// 50% + destinationVenueBalance = getVenueDeviation(destination, totalPcv); /// -30% } /// validate pcv movement /// check underlying assets match up and if not that swapper is provided and valid - _checkPCVMove( - source, - destination, - swapper, - sourceAsset, - destinationAsset - ); - - /// optimistically transfer funds to the specified pcv deposit - _movePCV( + PCVRouter(pcvRouter).movePCV( source, destination, swapper, @@ -565,8 +486,11 @@ contract MarketGovernance is CoreRefV2, PCVMover, IMarketGovernance { if (!ignoreBalanceChecks) { /// TODO simplify this by passing delta of starting balance instead of calling balance again on each pcv deposit /// record how balanced the system is before the PCV movement - int256 sourceVenueBalanceAfter = getVenueBalance(source, totalPcv); - int256 destinationVenueBalanceAfter = getVenueBalance( + int256 sourceVenueBalanceAfter = getVenueDeviation( + source, + totalPcv + ); + int256 destinationVenueBalanceAfter = getVenueDeviation( destination, totalPcv ); @@ -606,48 +530,60 @@ contract MarketGovernance is CoreRefV2, PCVMover, IMarketGovernance { /// @notice VCON rewards could be negative if a user is at a loss /// @param user to check rewards from /// @param venue to check rewards in + /// return the amount of pending rewards or losses for a given user + /// algorithm: + /// 1. Determine venue profits or losses that have occured since + /// the last time the user staked or unstaked in a given venue. + /// venue pnl = current profit index - user starting index + /// 2. Convert PnL to their pro-rata VCON amount + /// vcon rewards = venue PnL * user vcon deposited in venue / total vcon deposited in venue function getPendingRewards( address user, address venue ) public view returns (int256) { - int256 vconStartIndex = venueUserStartingVconProfit[venue][user] + /// TODO this is 5 SLOAD's and we can do better + /// Pack venue info down into a single struct to get us down to 3 SLOAD's + + int256 startingProfitIndex = venueUserStartingProfit[venue][user] .toInt256(); - int256 vconCurrentIndex = venueLastRecordedSharePrice[venue].toInt256(); + int256 currentProfitIndex = venueLastRecordedProfit[venue].toInt256(); + int256 profitToVcon = profitToVconRatio[venue].toInt256(); + int256 venueVconAmount = venueVconDeposited[venue].toInt256(); - if (vconStartIndex == 0) { + if (startingProfitIndex == 0 || venueVconAmount == 0) { return 0; /// no interest if user has not entered the market } - int256 vconRewards = (vconCurrentIndex - vconStartIndex) * - venueUserDepositedVcon[venue][user].toInt256(); + int256 currentProfits = currentProfitIndex - startingProfitIndex; + int256 vconRewards = (currentProfits * + venueUserDepositedVcon[venue][user].toInt256() * + profitToVcon) / venueVconAmount; return vconRewards; } - /// @notice returns profit in VCON a user has accrued + /// @notice returns profit or losses a VCON staker has accrued + /// updates their venue starting profit index /// does not update how much VCON a user has staked to save on gas /// that updating happens in the calling function function _harvestRewards( address user, address venue ) private returns (int256) { - uint256 vconCurrentIndex = venueLastRecordedSharePrice[venue]; + uint256 currentProfitIndex = venueLastRecordedProfit[venue]; - console.log("getting pending rewards"); /// get pending rewards int256 pendingRewardBalance = getPendingRewards(user, venue); console.log("got pending rewards", pendingRewardBalance.toUint256()); /// then set the vcon current index for this user - venueUserStartingVconProfit[venue][user] = vconCurrentIndex; + venueUserStartingProfit[venue][user] = currentProfitIndex; return pendingRewardBalance; } /// update the venue last recorded share price function _accrue(address venue) private { - uint256 startingLastRecordedProfit = venueLastRecordedProfit[venue]; - IPCVDepositV2(venue).accrue(); uint256 endingLastRecordedProfit = IPCVDepositV2(venue) @@ -656,30 +592,9 @@ contract MarketGovernance is CoreRefV2, PCVMover, IMarketGovernance { /// update venue last recorded profit regardless /// of participation in market governance venueLastRecordedProfit[venue] = endingLastRecordedProfit.toUint128(); - - /// amount of VCON that each VCON deposit receives for being in this venue - /// if there is no supply, there is no delta to apply - if (vconStaked != 0) { - int256 deltaProfit = endingLastRecordedProfit.toInt256() - - startingLastRecordedProfit.toInt256(); /// also could be a loss - - int256 venueProfitToVconRatio = profitToVconRatio[venue].toInt256(); - - int256 profitPerVconDelta = (deltaProfit * venueProfitToVconRatio) / - vconStaked.toInt256(); - - uint256 lastVconSharePrice = venueLastRecordedSharePrice[venue]; - require( - lastVconSharePrice >= Constants.ETH_GRANULARITY, - "MarketGovernance: venue not initialized" - ); - - venueLastRecordedSharePrice[venue] = (lastVconSharePrice - .toInt256() + profitPerVconDelta).toUint256().toUint128(); - } } - /// TODO add events + /// TODO add events to all functions /// ---------- Governor-Only Permissioned API ---------- @@ -696,4 +611,11 @@ contract MarketGovernance is CoreRefV2, PCVMover, IMarketGovernance { newProfitToVconRatio ); } + + function setPCVRouter(address newPcvRouter) external onlyGovernor { + address oldPcvRouter = pcvRouter; + pcvRouter = newPcvRouter; + + emit PCVRouterUpdated(oldPcvRouter, newPcvRouter); + } } From 659d6a87747be233a779f3f58268c20be57e04bf Mon Sep 17 00:00:00 2001 From: Elliot Date: Wed, 25 Jan 2023 22:41:24 -0800 Subject: [PATCH 31/74] delete mock pcv router --- test/mock/MockPCVRouter.sol | 39 ------------------------------------- 1 file changed, 39 deletions(-) delete mode 100644 test/mock/MockPCVRouter.sol diff --git a/test/mock/MockPCVRouter.sol b/test/mock/MockPCVRouter.sol deleted file mode 100644 index 171659c49..000000000 --- a/test/mock/MockPCVRouter.sol +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.13; - -import {PCVRouter} from "@voltprotocol/pcv/PCVRouter.sol"; -import {VoltRoles} from "@voltprotocol/core/VoltRoles.sol"; - -contract MockPCVRouter is PCVRouter { - constructor(address _core) PCVRouter(_core) {} - - function movePCV( - address source, - address destination, - address swapper, - uint256 amount, - address sourceAsset, - address destinationAsset - ) external onlyVoltRole(VoltRoles.PCV_MOVER) whenNotPaused globalLock(1) { - /// validate pcv movement - /// check underlying assets match up and if not that swapper is provided and valid - _checkPCVMove( - source, - destination, - swapper, - sourceAsset, - destinationAsset - ); - - /// optimistically transfer funds to the specified pcv deposit - /// swapper validity not checked in this contract as the PCV Router will check this - _movePCV( - source, - destination, - swapper, - amount, - sourceAsset, - destinationAsset - ); - } -} From 06b5f8b5080fc4456ea3edf4a1b284cccf3aee81 Mon Sep 17 00:00:00 2001 From: Elliot Date: Wed, 25 Jan 2023 22:41:57 -0800 Subject: [PATCH 32/74] Rollback PCVRouter tests, accomodate new movePCV arch --- test/unit/pcv/PCVRouter.t.sol | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/test/unit/pcv/PCVRouter.t.sol b/test/unit/pcv/PCVRouter.t.sol index a5161757d..aa8bc41a3 100644 --- a/test/unit/pcv/PCVRouter.t.sol +++ b/test/unit/pcv/PCVRouter.t.sol @@ -8,10 +8,10 @@ import {PCVOracle} from "@voltprotocol/oracle/PCVOracle.sol"; import {MockERC20} from "@test/mock/MockERC20.sol"; import {VoltRoles} from "@voltprotocol/core/VoltRoles.sol"; import {getCoreV2} from "@test/unit/utils/Fixtures.sol"; +import {PCVRouter} from "@voltprotocol/pcv/PCVRouter.sol"; import {IPCVOracle} from "@voltprotocol/oracle/IPCVOracle.sol"; import {MockOracle} from "@test/mock/MockOracle.sol"; import {SystemEntry} from "@voltprotocol/entry/SystemEntry.sol"; -import {MockPCVRouter} from "@test/mock/MockPCVRouter.sol"; import {MockPCVSwapper} from "@test/mock/MockPCVSwapper.sol"; import {MockPCVDepositV3} from "@test/mock/MockPCVDepositV3.sol"; import {TestAddresses as addresses} from "@test/unit/utils/TestAddresses.sol"; @@ -25,7 +25,7 @@ contract PCVRouterUnitTest is Test { PCVOracle private pcvOracle; // reference to the volt pcv router - MockPCVRouter private pcvRouter; + PCVRouter private pcvRouter; // global reentrancy lock IGlobalReentrancyLock private lock; @@ -72,7 +72,7 @@ contract PCVRouterUnitTest is Test { // volt system core = CoreV2(address(getCoreV2())); pcvOracle = new PCVOracle(address(core)); - pcvRouter = new MockPCVRouter(address(core)); + pcvRouter = new PCVRouter(address(core)); entry = new SystemEntry(address(core)); lock = IGlobalReentrancyLock( address(new GlobalReentrancyLock(address(core))) @@ -100,6 +100,7 @@ contract PCVRouterUnitTest is Test { core.grantLocker(address(depositToken2A)); core.grantLocker(address(depositToken2B)); core.grantLocker(address(pcvRouter)); + core.grantLocker(address(this)); core.grantPCVController(address(pcvRouter)); core.createRole(VoltRoles.PCV_MOVER, VoltRoles.GOVERNOR); core.grantRole(VoltRoles.PCV_MOVER, address(this)); @@ -216,6 +217,8 @@ contract PCVRouterUnitTest is Test { // ------------------------------------------------- function testMovePCVEvents() public { + lock.lock(1); + // PCVMovement vm.expectEmit(true, true, false, true, address(pcvRouter)); emit PCVMovement( @@ -239,6 +242,8 @@ contract PCVRouterUnitTest is Test { assertEq(depositToken1A.balance(), 100e18); assertEq(depositToken1B.balance(), 100e18); + lock.lock(1); + vm.expectCall( address(depositToken1A), abi.encodeWithSignature( @@ -268,6 +273,8 @@ contract PCVRouterUnitTest is Test { assertEq(depositToken1A.balance(), 100e18); assertEq(depositToken2A.balance(), 100e18); + lock.lock(1); + vm.prank(addresses.governorAddress); address[] memory whitelistedSwapperAddresses = new address[](1); whitelistedSwapperAddresses[0] = address(swapper); @@ -362,6 +369,7 @@ contract PCVRouterUnitTest is Test { // ------------------------------------------------- function testMovePCVInvalidSource() public { + lock.lock(1); vm.expectRevert("PCVRouter: invalid source"); pcvRouter.movePCV( address(this), // source, not a PCVDeposit @@ -374,6 +382,7 @@ contract PCVRouterUnitTest is Test { } function testMovePCVInvalidDestination() public { + lock.lock(1); vm.expectRevert("PCVRouter: invalid destination"); pcvRouter.movePCV( address(depositToken1A), // source @@ -386,6 +395,8 @@ contract PCVRouterUnitTest is Test { } function testMovePCVWrongToken() public { + lock.lock(1); + vm.expectRevert("PCVRouter: invalid source asset"); pcvRouter.movePCV( address(depositToken1A), // source @@ -408,6 +419,8 @@ contract PCVRouterUnitTest is Test { } function testMovePCVInvalidSwapper() public { + lock.lock(1); + vm.expectRevert("PCVRouter: invalid swapper"); pcvRouter.movePCV( address(depositToken1A), // source @@ -427,6 +440,8 @@ contract PCVRouterUnitTest is Test { whitelistedSwapperAddresses[0] = address(swapper); pcvRouter.addPCVSwappers(whitelistedSwapperAddresses); + lock.lock(1); + vm.expectRevert("PCVRouter: unsupported swap"); pcvRouter.movePCV( address(depositToken2A), // source @@ -531,8 +546,8 @@ contract PCVRouterUnitTest is Test { function testMovePCVRevertIfNotPCVController() public { vm.prank(addresses.governorAddress); core.revokePCVController(address(pcvRouter)); - depositToken1A.setCheckPCVController(true); + lock.lock(1); vm.expectRevert("CoreRef: Caller is not a PCV controller"); pcvRouter.movePCV( @@ -545,11 +560,11 @@ contract PCVRouterUnitTest is Test { ); } - function testMovePCVRevertIfNotLocker() public { + function testMovePCVRevertIfNotLocked() public { vm.prank(addresses.governorAddress); core.revokeLocker(address(pcvRouter)); - vm.expectRevert("UNAUTHORIZED"); + vm.expectRevert("CoreRef: System not at lock level"); pcvRouter.movePCV( address(depositToken1A), // source address(depositToken1B), // destination From f07f7deb374aafd253cddf2f78bd24c38a8530d9 Mon Sep 17 00:00:00 2001 From: Elliot Date: Wed, 25 Jan 2023 22:42:19 -0800 Subject: [PATCH 33/74] Update Market Governance tests --- test/unit/vcon/MarketGovernance.t.sol | 211 +++++++++++++------------- 1 file changed, 102 insertions(+), 109 deletions(-) diff --git a/test/unit/vcon/MarketGovernance.t.sol b/test/unit/vcon/MarketGovernance.t.sol index 0199a72af..1484500b3 100644 --- a/test/unit/vcon/MarketGovernance.t.sol +++ b/test/unit/vcon/MarketGovernance.t.sol @@ -3,15 +3,18 @@ pragma solidity 0.8.13; import {console} from "@forge-std/console.sol"; import {MockERC20} from "@test/mock/MockERC20.sol"; import {VoltRoles} from "@voltprotocol/core/VoltRoles.sol"; +import {PCVRouter} from "@voltprotocol/pcv/PCVRouter.sol"; import {MockPCVSwapper} from "@test/mock/MockPCVSwapper.sol"; import {SystemUnitTest} from "@test/unit/system/System.t.sol"; import {MockPCVDepositV3} from "@test/mock/MockPCVDepositV3.sol"; import {MarketGovernance} from "@voltprotocol/vcon/MarketGovernance.sol"; +import {IMarketGovernance} from "@voltprotocol/vcon/IMarketGovernance.sol"; import {TestAddresses as addresses} from "@test/unit/utils/TestAddresses.sol"; contract UnitTestMarketGovernance is SystemUnitTest { MarketGovernance public mgov; MockPCVSwapper public pcvSwapper; + address public venue = address(10); /// a in hex uint256 public profitToVconRatio = 5; /// for each 1 wei in profit, 5 wei of vcon is received uint256 public daiDepositAmount = 1_000_000e18; @@ -31,7 +34,9 @@ contract UnitTestMarketGovernance is SystemUnitTest { ); pcvSwapper.mockSetExchangeRate(1e6); /// set dai->usdc exchange rate - mgov = new MarketGovernance(coreAddress); + pcvRouter = new PCVRouter(coreAddress); + + mgov = new MarketGovernance(coreAddress, address(pcvRouter)); vm.startPrank(addresses.governorAddress); @@ -42,15 +47,16 @@ contract UnitTestMarketGovernance is SystemUnitTest { address[] memory swapper = new address[](1); swapper[0] = address(pcvSwapper); - mgov.addPCVSwappers(swapper); + + pcvRouter.addPCVSwappers(swapper); + + core.createRole(VoltRoles.PCV_MOVER, VoltRoles.GOVERNOR); + core.grantRole(VoltRoles.PCV_MOVER, address(mgov)); vm.stopPrank(); } function _initializeVenues() private { - mgov.initializeVenue(address(pcvDepositDai)); - mgov.initializeVenue(address(pcvDepositUsdc)); - pcvDepositUsdc.setLastRecordedProfit(10_000e18); pcvDepositDai.setLastRecordedProfit(10_000e18); } @@ -81,13 +87,17 @@ contract UnitTestMarketGovernance is SystemUnitTest { vcon.mint(address(this), vconDepositAmount); vcon.approve(address(mgov), vconDepositAmount); - mgov.stake( - vconDepositAmount, - pcvDepositDai.balance(), - address(pcvDepositDai), - address(pcvDepositUsdc), - address(pcvSwapper) - ); + mgov.stake(vconDepositAmount, address(pcvDepositUsdc)); + + IMarketGovernance.Rebalance[] + memory balance = new IMarketGovernance.Rebalance[](1); + balance[0] = IMarketGovernance.Rebalance({ + source: address(pcvDepositDai), + destination: address(pcvDepositUsdc), + swapper: address(pcvSwapper), + amountPcv: pcvDepositDai.balance() + }); + mgov.rebalance(balance); assertEq( pcvOracle.getTotalPcv(), @@ -113,13 +123,16 @@ contract UnitTestMarketGovernance is SystemUnitTest { vcon.mint(userOne, vconDepositAmount); vcon.approve(address(mgov), vconDepositAmount); - mgov.stake( - vconDepositAmount, - pcvDepositUsdc.balance() / 2, - address(pcvDepositUsdc), - address(pcvDepositDai), - address(pcvSwapper) - ); + mgov.stake(vconDepositAmount, address(pcvDepositDai)); + IMarketGovernance.Rebalance[] + memory balance = new IMarketGovernance.Rebalance[](1); + balance[0] = IMarketGovernance.Rebalance({ + source: address(pcvDepositUsdc), + destination: address(pcvDepositDai), + swapper: address(pcvSwapper), + amountPcv: pcvDepositUsdc.balance() / 2 + }); + mgov.rebalance(balance); vm.stopPrank(); @@ -139,30 +152,38 @@ contract UnitTestMarketGovernance is SystemUnitTest { testSystemTwoUsers(); uint256 startingTotalSupply = mgov.vconStaked(); + uint256 startingVconStaked = mgov.venueVconDeposited( + address(pcvDepositUsdc) + ); vm.startPrank(userTwo); vcon.mint(userTwo, vconAmount); vcon.approve(address(mgov), vconAmount); - mgov.stake( - vconAmount, - 0, /// deposit no pcv, meaning things will be imbalanced - address(pcvDepositDai), - address(pcvDepositUsdc), - address(pcvSwapper) - ); + mgov.stake(vconAmount, address(pcvDepositUsdc)); vm.stopPrank(); assertEq(vcon.balanceOf(userTwo), 0); - assertEq(mgov.venueVconDeposited(userTwo), vconAmount); + assertEq( + mgov.venueVconDeposited(address(pcvDepositUsdc)), + vconAmount + startingVconStaked + ); + assertEq( + mgov.venueUserDepositedVcon(address(pcvDepositUsdc), userTwo), + vconAmount + ); uint256 endingTotalSupply = mgov.vconStaked(); uint256 totalPCV = pcvOracle.getTotalPcv(); assertEq(endingTotalSupply, startingTotalSupply + vconAmount); - assertTrue(mgov.getVenueBalance(address(pcvDepositUsdc), totalPCV) < 0); /// underweight USDC balance - assertTrue(mgov.getVenueBalance(address(pcvDepositDai), totalPCV) > 0); /// overweight DAI balance + assertTrue( + mgov.getVenueDeviation(address(pcvDepositUsdc), totalPCV) < 0 + ); /// underweight USDC balance + assertTrue( + mgov.getVenueDeviation(address(pcvDepositDai), totalPCV) > 0 + ); /// overweight DAI balance } function testSystemThreeUsersLastNoDepositIndividual() public { @@ -173,15 +194,11 @@ contract UnitTestMarketGovernance is SystemUnitTest { uint256 startingTotalSupply = mgov.vconStaked(); vm.startPrank(user); + vcon.mint(user, vconAmount); vcon.approve(address(mgov), vconAmount); - mgov.stake( - vconAmount, - 0, /// deposit no pcv, meaning things will be imbalanced - address(pcvDepositDai), - address(pcvDepositUsdc), - address(pcvSwapper) - ); + mgov.stake(vconAmount, address(pcvDepositUsdc)); + vm.stopPrank(); uint256 endingTotalSupply = mgov.vconStaked(); @@ -189,8 +206,12 @@ contract UnitTestMarketGovernance is SystemUnitTest { assertEq(vcon.balanceOf(user), 0); assertEq(endingTotalSupply, startingTotalSupply + vconAmount); - assertTrue(mgov.getVenueBalance(address(pcvDepositUsdc), totalPCV) < 0); /// underweight USDC balance - assertTrue(mgov.getVenueBalance(address(pcvDepositDai), totalPCV) > 0); /// overweight DAI balance + assertTrue( + mgov.getVenueDeviation(address(pcvDepositUsdc), totalPCV) < 0 + ); /// underweight USDC balance + assertTrue( + mgov.getVenueDeviation(address(pcvDepositDai), totalPCV) > 0 + ); /// overweight DAI balance } function testUserDepositsNoMove(uint120 vconAmount) public { @@ -204,13 +225,7 @@ contract UnitTestMarketGovernance is SystemUnitTest { vm.startPrank(user); vcon.mint(user, vconAmount); vcon.approve(address(mgov), vconAmount); - mgov.stake( - vconAmount, - 0, /// deposit no pcv, meaning things will be imbalanced - address(pcvDepositDai), - address(pcvDepositUsdc), - address(pcvSwapper) - ); + mgov.stake(vconAmount, address(pcvDepositUsdc)); vm.stopPrank(); uint256 endingTotalSupply = mgov.vconStaked(); @@ -218,53 +233,48 @@ contract UnitTestMarketGovernance is SystemUnitTest { assertEq(vcon.balanceOf(user), 0); assertEq(endingTotalSupply, startingTotalSupply + vconAmount); - assertTrue(mgov.getVenueBalance(address(pcvDepositUsdc), totalPCV) < 0); /// underweight USDC - assertTrue(mgov.getVenueBalance(address(pcvDepositDai), totalPCV) > 0); /// overweight DAI - } - - function testUserDepositsFailsNotInitialized() public { - mgov.initializeVenue(address(pcvDepositUsdc)); - - address user = address(1001); - uint120 vconAmount = 1e18; - - vm.startPrank(user); - vcon.mint(user, vconAmount); - vcon.approve(address(mgov), vconAmount); - mgov.stake( - vconAmount, - 0, - address(pcvDepositDai), - address(pcvDepositUsdc), - address(pcvSwapper) - ); - vm.stopPrank(); - - vm.expectRevert("MarketGovernance: venue not initialized"); - mgov.stake( - vconAmount, - 1e18, - address(pcvDepositUsdc), - address(pcvDepositDai), - address(pcvSwapper) - ); - - vm.expectRevert("MarketGovernance: venue not initialized"); - mgov.stake( - vconAmount, - 0, /// deposit no pcv, meaning things will be imbalanced - address(pcvDepositUsdc), - address(pcvDepositDai), - address(pcvSwapper) - ); + assertTrue( + mgov.getVenueDeviation(address(pcvDepositUsdc), totalPCV) < 0 + ); /// underweight USDC + assertTrue( + mgov.getVenueDeviation(address(pcvDepositDai), totalPCV) > 0 + ); /// overweight DAI } - function testReinitializingVenueFails() public { - _initializeVenues(); - - vm.expectRevert("MarketGovernance: venue already has share price"); - mgov.initializeVenue(address(pcvDepositUsdc)); - } + // function testUserDepositsFailsNotInitialized() public { + // address user = address(1001); + // uint120 vconAmount = 1e18; + + // vm.startPrank(user); + // vcon.mint(user, vconAmount); + // vcon.approve(address(mgov), vconAmount); + // mgov.stake( + // vconAmount, + // 0, + // address(pcvDepositDai), + // address(pcvDepositUsdc), + // address(pcvSwapper) + // ); + // vm.stopPrank(); + + // vm.expectRevert("MarketGovernance: venue not initialized"); + // mgov.stake( + // vconAmount, + // 1e18, + // address(pcvDepositUsdc), + // address(pcvDepositDai), + // address(pcvSwapper) + // ); + + // vm.expectRevert("MarketGovernance: venue not initialized"); + // mgov.stake( + // vconAmount, + // 0, /// deposit no pcv, meaning things will be imbalanced + // address(pcvDepositUsdc), + // address(pcvDepositDai), + // address(pcvSwapper) + // ); + // } function testUnstakingOneUser() public { testSystemOneUser(); @@ -321,15 +331,12 @@ contract UnitTestMarketGovernance is SystemUnitTest { pcvOracle.getTotalPcv(), pcvOracle.getVenueBalance(address(pcvDepositDai)) ); - assertEq( - vconAmount - startingVconStakedAmount, - mgov.venueVconDeposited(address(pcvDepositDai)) - ); + assertEq(startingVconStakedAmount - vconAmount, mgov.vconStaked()); assertEq(0, pcvOracle.getVenueBalance(address(pcvDepositUsdc))); assertEq(0, mgov.venueVconDeposited(address(pcvDepositUsdc))); - assertEq(vconAmount - startingVconStakedAmount, mgov.vconStaked()); + // assertEq(vconAmount - startingVconStakedAmount, mgov.vconStaked()); } /// test withdrawing when src and dest are equal @@ -346,20 +353,6 @@ contract UnitTestMarketGovernance is SystemUnitTest { ); } - /// test depositing when src and dest are equal - function testDepositFailsSrcDestEqual() public { - uint256 vconAmount = mgov.vconStaked(); - - vm.expectRevert("MarketGovernance: src and dest equal"); - mgov.stake( - vconAmount, - 0, - address(pcvDepositUsdc), - address(pcvDepositUsdc), - address(0) - ); - } - /// todo test withdrawing /// todo test depositing fails when venue not initialized /// todo test withdrawing when there are profits From 3f6c40232799620fa2061184b2600758195a18d8 Mon Sep 17 00:00:00 2001 From: Elliot Date: Wed, 25 Jan 2023 23:16:50 -0800 Subject: [PATCH 34/74] add events, update tests --- src/vcon/MarketGovernance.sol | 133 +++++++++++++++++--------- test/unit/vcon/MarketGovernance.t.sol | 10 ++ 2 files changed, 96 insertions(+), 47 deletions(-) diff --git a/src/vcon/MarketGovernance.sol b/src/vcon/MarketGovernance.sol index ac9ca93a2..c25124459 100644 --- a/src/vcon/MarketGovernance.sol +++ b/src/vcon/MarketGovernance.sol @@ -45,6 +45,35 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { uint256 newRatio ); + /// @notice emitted when a loss is realized + event LossRealized( + address indexed venue, + address indexed user, + int256 amount + ); + + /// @notice emitted when a gain is realized + event GainRealized( + address indexed venue, + address indexed user, + uint256 amount + ); + + /// @notice emitted whenever a user stakes + event Staked( + address indexed venue, + address indexed user, + uint256 vconAmount + ); + + /// @notice emitted whenever a user unstakes + event Unstaked( + address indexed venue, + address indexed user, + uint256 vconAmount, + uint256 pcvAmount + ); + /// @notice reference to the PCV Router address public pcvRouter; @@ -326,62 +355,72 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { address[] calldata venues ) external globalLock(1) { uint256 venueLength = venues.length; - int256 totalVcon = 0; + uint256 totalVcon = 0; - unchecked { - for (uint256 i = 0; i < venueLength; i++) { - address venue = venues[i]; - require( - pcvOracle().isVenue(venue), - "MarketGovernance: invalid venue" - ); + for (uint256 i = 0; i < venueLength; ) { + address venue = venues[i]; + require( + pcvOracle().isVenue(venue), + "MarketGovernance: invalid venue" + ); - /// updates the venueLastRecordedProfit - _accrue(venue); - - /// updates the venueUserStartingProfit mapping - int256 pnl = _harvestRewards(msg.sender, venue); - if (pnl < 0) { - /// loss scenarios - if ( - (-pnl).toUint256() > - venueUserDepositedVcon[venue][msg.sender] - ) { - uint256 userDepositedAmount = venueUserDepositedVcon[ - venue - ][msg.sender]; - /// zero the user's balance - venueUserDepositedVcon[venue][msg.sender] = 0; - - /// take losses off the total amount staked - vconStaked -= userDepositedAmount; - - /// TODO final write to storage, decrement venue deposited VCON - venueVconDeposited[venue] -= userDepositedAmount; - } else { - /// losses should never exceed staked amount at this point - venueUserDepositedVcon[venue][ - msg.sender - ] = (venueUserDepositedVcon[venue][msg.sender] - .toInt256() + pnl).toUint256(); - - /// take losses off the total amount staked - vconStaked = (vconStaked.toInt256() + pnl).toUint256(); - - /// TODO final write to storage, decrement venue deposited VCON - venueVconDeposited[venue] -= (-pnl).toUint256(); - } + /// updates the venueLastRecordedProfit + _accrue(venue); + + /// updates the venueUserStartingProfit mapping + int256 pnl = _harvestRewards(msg.sender, venue); + if (pnl < 0) { + int256 lossAmount; + /// loss scenarios + if ( + (-pnl).toUint256() > + venueUserDepositedVcon[venue][msg.sender] + ) { + uint256 userDepositedAmount = venueUserDepositedVcon[venue][ + msg.sender + ]; + /// zero the user's balance + venueUserDepositedVcon[venue][msg.sender] = 0; + + /// take losses off the total amount staked + vconStaked -= userDepositedAmount; + + /// TODO final write to storage, decrement venue deposited VCON + venueVconDeposited[venue] -= userDepositedAmount; + + lossAmount = -(userDepositedAmount).toInt256(); } else { - /// gain or even scenario - totalVcon += pnl; + /// losses should never exceed staked amount at this point + venueUserDepositedVcon[venue][ + msg.sender + ] = (venueUserDepositedVcon[venue][msg.sender].toInt256() + + pnl).toUint256(); + + /// take losses off the total amount staked + vconStaked = (vconStaked.toInt256() + pnl).toUint256(); + + /// decrement venue deposited VCON + venueVconDeposited[venue] -= (-pnl).toUint256(); + + lossAmount = pnl; } - /// TODO emit an event + emit LossRealized(venue, msg.sender, lossAmount); + } else { + /// gain or even scenario + totalVcon += pnl.toUint256(); + if (pnl != 0) { + emit GainRealized(venue, msg.sender, pnl.toUint256()); + } + } + + unchecked { + i++; } } if (totalVcon != 0) { - vcon().safeTransfer(msg.sender, totalVcon.toUint256()); + vcon().safeTransfer(msg.sender, totalVcon); } } diff --git a/test/unit/vcon/MarketGovernance.t.sol b/test/unit/vcon/MarketGovernance.t.sol index 1484500b3..0ae53f929 100644 --- a/test/unit/vcon/MarketGovernance.t.sol +++ b/test/unit/vcon/MarketGovernance.t.sol @@ -357,4 +357,14 @@ contract UnitTestMarketGovernance is SystemUnitTest { /// todo test depositing fails when venue not initialized /// todo test withdrawing when there are profits /// todo test withdrawing when there are losses + + function testSetProfitToVconRatioFailsNonGovernor() public { + vm.expectRevert("CoreRef: Caller is not a governor"); + mgov.setProfitToVconRatio(address(0), 0); + } + + function testSetPCVRouterFailsNonGovernor() public { + vm.expectRevert("CoreRef: Caller is not a governor"); + mgov.setPCVRouter(address(0)); + } } From bc21de7cef656744631a852dc14093e6edeb514a Mon Sep 17 00:00:00 2001 From: Elliot Date: Wed, 25 Jan 2023 23:36:34 -0800 Subject: [PATCH 35/74] events --- src/vcon/IMarketGovernance.sol | 49 ++++++++++++ src/vcon/MarketGovernance.sol | 133 ++++++++++++--------------------- 2 files changed, 97 insertions(+), 85 deletions(-) diff --git a/src/vcon/IMarketGovernance.sol b/src/vcon/IMarketGovernance.sol index 3b8c5eb0b..10b052faa 100644 --- a/src/vcon/IMarketGovernance.sol +++ b/src/vcon/IMarketGovernance.sol @@ -30,6 +30,55 @@ interface IMarketGovernance { uint256 vconAmount ); + /// @notice emitted when the router is updated + event PCVRouterUpdated( + address indexed oldPcvRouter, + address indexed newPcvRouter + ); + + /// @notice emitted when profit to vcon ratio is updated + event ProfitToVconRatioUpdated( + address indexed venue, + uint256 oldRatio, + uint256 newRatio + ); + + /// @notice emitted when a loss is realized + event LossRealized( + address indexed venue, + address indexed user, + uint256 vconLossAmount + ); + + /// @notice emitted whenever a user stakes + event Staked( + address indexed venue, + address indexed user, + uint256 vconAmount + ); + + /// @notice emitted whenever a user unstakes + event Unstaked( + address indexed venue, + address indexed user, + uint256 vconAmount, + uint256 pcvAmount + ); + + /// @notice emitted whenever a user harvests + event Harvest( + address indexed venue, + address indexed user, + uint256 vconAmount + ); + + /// @notice emitted when a venue's index is updated via accrue + event VenueIndexUpdated( + address indexed venue, + uint256 indexed timestamp, + uint256 profitIndex + ); + /// ---------- Permissionless User PCV Allocation Methods ---------- /// stake VCON on a venue diff --git a/src/vcon/MarketGovernance.sol b/src/vcon/MarketGovernance.sol index c25124459..2b3a4fbeb 100644 --- a/src/vcon/MarketGovernance.sol +++ b/src/vcon/MarketGovernance.sol @@ -32,48 +32,6 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { using SafeERC20 for *; using SafeCast for *; - /// @notice emitted when the router is updated - event PCVRouterUpdated( - address indexed oldPcvRouter, - address indexed newPcvRouter - ); - - /// @notice emitted when profit to vcon ratio is updated - event ProfitToVconRatioUpdated( - address indexed venue, - uint256 oldRatio, - uint256 newRatio - ); - - /// @notice emitted when a loss is realized - event LossRealized( - address indexed venue, - address indexed user, - int256 amount - ); - - /// @notice emitted when a gain is realized - event GainRealized( - address indexed venue, - address indexed user, - uint256 amount - ); - - /// @notice emitted whenever a user stakes - event Staked( - address indexed venue, - address indexed user, - uint256 vconAmount - ); - - /// @notice emitted whenever a user unstakes - event Unstaked( - address indexed venue, - address indexed user, - uint256 vconAmount, - uint256 pcvAmount - ); - /// @notice reference to the PCV Router address public pcvRouter; @@ -251,8 +209,6 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { } } - /// TODO add slashing logic to burn VCON from participants that are in a venue that took a loss - /// ------------- View Only Methods ------------- /// @notice returns positive value if over allocated @@ -370,7 +326,7 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { /// updates the venueUserStartingProfit mapping int256 pnl = _harvestRewards(msg.sender, venue); if (pnl < 0) { - int256 lossAmount; + uint256 lossAmount; /// loss scenarios if ( (-pnl).toUint256() > @@ -385,10 +341,10 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { /// take losses off the total amount staked vconStaked -= userDepositedAmount; - /// TODO final write to storage, decrement venue deposited VCON + /// final write to storage, decrement venue deposited VCON venueVconDeposited[venue] -= userDepositedAmount; - lossAmount = -(userDepositedAmount).toInt256(); + lossAmount = userDepositedAmount; } else { /// losses should never exceed staked amount at this point venueUserDepositedVcon[venue][ @@ -402,7 +358,7 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { /// decrement venue deposited VCON venueVconDeposited[venue] -= (-pnl).toUint256(); - lossAmount = pnl; + lossAmount = (-pnl).toUint256(); } emit LossRealized(venue, msg.sender, lossAmount); @@ -410,7 +366,7 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { /// gain or even scenario totalVcon += pnl.toUint256(); if (pnl != 0) { - emit GainRealized(venue, msg.sender, pnl.toUint256()); + emit Harvest(venue, msg.sender, pnl.toUint256()); } } @@ -481,6 +437,41 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { } } + /// @notice VCON rewards could be negative if a user is at a loss + /// @param user to check rewards from + /// @param venue to check rewards in + /// return the amount of pending rewards or losses for a given user + /// algorithm: + /// 1. Determine venue profits or losses that have occured since + /// the last time the user staked or unstaked in a given venue. + /// venue pnl = current profit index - user starting index + /// 2. Convert PnL to their pro-rata VCON amount + /// vcon rewards = venue PnL * user vcon deposited in venue / total vcon deposited in venue + function getPendingRewards( + address user, + address venue + ) public view returns (int256) { + /// TODO this is 5 SLOAD's and we can do better + /// Pack venue info down into a single struct to get us down to 3 SLOAD's + + int256 startingProfitIndex = venueUserStartingProfit[venue][user] + .toInt256(); + int256 currentProfitIndex = venueLastRecordedProfit[venue].toInt256(); + int256 profitToVcon = profitToVconRatio[venue].toInt256(); + int256 venueVconAmount = venueVconDeposited[venue].toInt256(); + + if (startingProfitIndex == 0 || venueVconAmount == 0) { + return 0; /// no interest if user has not entered the market + } + + int256 currentProfits = currentProfitIndex - startingProfitIndex; + int256 vconRewards = (currentProfits * + venueUserDepositedVcon[venue][user].toInt256() * + profitToVcon) / venueVconAmount; + + return vconRewards; + } + /// ------------- Helper Methods ------------- /// @param source address to pull funds from @@ -523,7 +514,8 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { ); if (!ignoreBalanceChecks) { - /// TODO simplify this by passing delta of starting balance instead of calling balance again on each pcv deposit + /// TODO simplify this by passing delta of starting balance + /// instead of calling balance again on each pcv deposit /// record how balanced the system is before the PCV movement int256 sourceVenueBalanceAfter = getVenueDeviation( source, @@ -566,41 +558,6 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { ); } - /// @notice VCON rewards could be negative if a user is at a loss - /// @param user to check rewards from - /// @param venue to check rewards in - /// return the amount of pending rewards or losses for a given user - /// algorithm: - /// 1. Determine venue profits or losses that have occured since - /// the last time the user staked or unstaked in a given venue. - /// venue pnl = current profit index - user starting index - /// 2. Convert PnL to their pro-rata VCON amount - /// vcon rewards = venue PnL * user vcon deposited in venue / total vcon deposited in venue - function getPendingRewards( - address user, - address venue - ) public view returns (int256) { - /// TODO this is 5 SLOAD's and we can do better - /// Pack venue info down into a single struct to get us down to 3 SLOAD's - - int256 startingProfitIndex = venueUserStartingProfit[venue][user] - .toInt256(); - int256 currentProfitIndex = venueLastRecordedProfit[venue].toInt256(); - int256 profitToVcon = profitToVconRatio[venue].toInt256(); - int256 venueVconAmount = venueVconDeposited[venue].toInt256(); - - if (startingProfitIndex == 0 || venueVconAmount == 0) { - return 0; /// no interest if user has not entered the market - } - - int256 currentProfits = currentProfitIndex - startingProfitIndex; - int256 vconRewards = (currentProfits * - venueUserDepositedVcon[venue][user].toInt256() * - profitToVcon) / venueVconAmount; - - return vconRewards; - } - /// @notice returns profit or losses a VCON staker has accrued /// updates their venue starting profit index /// does not update how much VCON a user has staked to save on gas @@ -631,6 +588,12 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { /// update venue last recorded profit regardless /// of participation in market governance venueLastRecordedProfit[venue] = endingLastRecordedProfit.toUint128(); + + emit VenueIndexUpdated( + venue, + block.timestamp, + endingLastRecordedProfit + ); } /// TODO add events to all functions From de7bc3f11fd2de99a1e77e3529818b597221ae47 Mon Sep 17 00:00:00 2001 From: Elliot Date: Wed, 25 Jan 2023 23:40:18 -0800 Subject: [PATCH 36/74] event in _harvestRewards --- src/vcon/MarketGovernance.sol | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/vcon/MarketGovernance.sol b/src/vcon/MarketGovernance.sol index 2b3a4fbeb..f45ebe557 100644 --- a/src/vcon/MarketGovernance.sol +++ b/src/vcon/MarketGovernance.sol @@ -575,6 +575,9 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { /// then set the vcon current index for this user venueUserStartingProfit[venue][user] = currentProfitIndex; + /// if there are losses, revert on SafeCast + emit Harvest(venue, user, pendingRewardBalance.toUint256()); + return pendingRewardBalance; } From b5e2536c1289ae22fcaae3039a6749ff47b2044d Mon Sep 17 00:00:00 2001 From: Elliot Date: Wed, 25 Jan 2023 23:44:13 -0800 Subject: [PATCH 37/74] Add events to stake() and unstake() --- src/vcon/MarketGovernance.sol | 164 +++++++++++++++++----------------- 1 file changed, 84 insertions(+), 80 deletions(-) diff --git a/src/vcon/MarketGovernance.sol b/src/vcon/MarketGovernance.sol index f45ebe557..1a8d77295 100644 --- a/src/vcon/MarketGovernance.sol +++ b/src/vcon/MarketGovernance.sol @@ -113,6 +113,8 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { /// check and an interaction with a trusted contract vcon().safeTransferFrom(msg.sender, address(this), amountVcon); /// transfer VCON in + + emit Staked(destination, msg.sender, amountVcon); } /// @notice unstake VCON and transfer corresponding VCON to another venue @@ -183,6 +185,8 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { amountPcv, ignoreBalanceChecks ); + + emit Unstaked(source, msg.sender, amountVcon, amountPcv); } /// @notice rebalance PCV without staking or unstaking VCON @@ -209,6 +213,86 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { } } + /// @notice realize gains and losses for msg.sender + /// @param venues to realize losses in + /// only the caller can realize losses on their own behalf + /// duplicating addresses does not allow theft as all venues have their indexes + /// updated before we find the profit and loss, so a duplicate venue will have 0 delta a second time + /// @dev we can't follow CEI here because we have to make external calls to update + /// the external venues. However, this is not an issue as the global reentrancy lock is enabled + function realizeGainsAndLosses( + address[] calldata venues + ) external globalLock(1) { + uint256 venueLength = venues.length; + uint256 totalVcon = 0; + + for (uint256 i = 0; i < venueLength; ) { + address venue = venues[i]; + require( + pcvOracle().isVenue(venue), + "MarketGovernance: invalid venue" + ); + + /// updates the venueLastRecordedProfit + _accrue(venue); + + /// updates the venueUserStartingProfit mapping + int256 pnl = _harvestRewards(msg.sender, venue); + if (pnl < 0) { + uint256 lossAmount; + /// loss scenarios + if ( + (-pnl).toUint256() > + venueUserDepositedVcon[venue][msg.sender] + ) { + uint256 userDepositedAmount = venueUserDepositedVcon[venue][ + msg.sender + ]; + /// zero the user's balance + venueUserDepositedVcon[venue][msg.sender] = 0; + + /// take losses off the total amount staked + vconStaked -= userDepositedAmount; + + /// final write to storage, decrement venue deposited VCON + venueVconDeposited[venue] -= userDepositedAmount; + + lossAmount = userDepositedAmount; + } else { + /// losses should never exceed staked amount at this point + venueUserDepositedVcon[venue][ + msg.sender + ] = (venueUserDepositedVcon[venue][msg.sender].toInt256() + + pnl).toUint256(); + + /// take losses off the total amount staked + vconStaked = (vconStaked.toInt256() + pnl).toUint256(); + + /// decrement venue deposited VCON + venueVconDeposited[venue] -= (-pnl).toUint256(); + + lossAmount = (-pnl).toUint256(); + } + + emit LossRealized(venue, msg.sender, lossAmount); + } else { + /// gain or even scenario + totalVcon += pnl.toUint256(); + if (pnl != 0) { + emit Harvest(venue, msg.sender, pnl.toUint256()); + } + } + + unchecked { + i++; + } + } + + if (totalVcon != 0) { + vcon().safeTransfer(msg.sender, totalVcon); + } + } + /// ------------- View Only Methods ------------- /// @notice returns positive value if over allocated @@ -300,86 +384,6 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { return proRataPcv; } - /// @notice realize gains and losses for msg.sender - /// @param venues to realize losses in - /// only the caller can realize losses on their own behalf - /// duplicating addresses does not allow theft as all venues have their indexes - /// updated before we find the profit and loss, so a duplicate venue will have 0 delta a second time - /// @dev we can't follow CEI here because we have to make external calls to update - /// the external venues. However, this is not an issue as the global reentrancy lock is enabled - function realizeGainsAndLosses( - address[] calldata venues - ) external globalLock(1) { - uint256 venueLength = venues.length; - uint256 totalVcon = 0; - - for (uint256 i = 0; i < venueLength; ) { - address venue = venues[i]; - require( - pcvOracle().isVenue(venue), - "MarketGovernance: invalid venue" - ); - - /// updates the venueLastRecordedProfit - _accrue(venue); - - /// updates the venueUserStartingProfit mapping - int256 pnl = _harvestRewards(msg.sender, venue); - if (pnl < 0) { - uint256 lossAmount; - /// loss scenarios - if ( - (-pnl).toUint256() > - venueUserDepositedVcon[venue][msg.sender] - ) { - uint256 userDepositedAmount = venueUserDepositedVcon[venue][ - msg.sender - ]; - /// zero the user's balance - venueUserDepositedVcon[venue][msg.sender] = 0; - - /// take losses off the total amount staked - vconStaked -= userDepositedAmount; - - /// final write to storage, decrement venue deposited VCON - venueVconDeposited[venue] -= userDepositedAmount; - - lossAmount = userDepositedAmount; - } else { - /// losses should never exceed staked amount at this point - venueUserDepositedVcon[venue][ - msg.sender - ] = (venueUserDepositedVcon[venue][msg.sender].toInt256() + - pnl).toUint256(); - - /// take losses off the total amount staked - vconStaked = (vconStaked.toInt256() + pnl).toUint256(); - - /// decrement venue deposited VCON - venueVconDeposited[venue] -= (-pnl).toUint256(); - - lossAmount = (-pnl).toUint256(); - } - - emit LossRealized(venue, msg.sender, lossAmount); - } else { - /// gain or even scenario - totalVcon += pnl.toUint256(); - if (pnl != 0) { - emit Harvest(venue, msg.sender, pnl.toUint256()); - } - } - - unchecked { - i++; - } - } - - if (totalVcon != 0) { - vcon().safeTransfer(msg.sender, totalVcon); - } - } - /// @notice return the amount of rewards accrued so far /// without calling accrue on the underlying venues function getAccruedRewards( From 7b8320c6c68b0f41e56c6826e603fd3e86761b32 Mon Sep 17 00:00:00 2001 From: Elliot Date: Thu, 26 Jan 2023 12:26:34 -0800 Subject: [PATCH 38/74] update internal function names, add natspec --- src/vcon/MarketGovernance.sol | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/vcon/MarketGovernance.sol b/src/vcon/MarketGovernance.sol index 1a8d77295..d3d243f36 100644 --- a/src/vcon/MarketGovernance.sol +++ b/src/vcon/MarketGovernance.sol @@ -13,8 +13,6 @@ import {IPCVDepositV2} from "@voltprotocol/pcv/IPCVDepositV2.sol"; import {IMarketGovernance} from "@voltprotocol/vcon/IMarketGovernance.sol"; import {DeviationWeiGranularity} from "@voltprotocol/utils/DeviationWeiGranularity.sol"; -import {console} from "@forge-std/console.sol"; - /// @notice this contract requires the PCV Mover and Locker role /// /// Core formula for market governance rewards: @@ -27,6 +25,9 @@ import {console} from "@forge-std/console.sol"; /// The VCON:Dollar ratio is the same for both profits and losses. If a venue has a VCON:Dollar ratio of 5:1 /// and the venue gains $5 in profits, then 25 VCON will be distributed across all VCON stakers in that venue. /// If that same venue losses $5, then a loss of 25 VCON will be distributed across all VCON stakers in that venue. +/// +/// @dev this contract assumes it is already topped up with the VCON necessary to pay rewards. +/// A dripper will keep this contract funded at a steady pace. contract MarketGovernance is CoreRefV2, IMarketGovernance { using DeviationWeiGranularity for *; using SafeERC20 for *; @@ -94,7 +95,7 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { ); _accrue(destination); /// update profitPerVCON in the destination so the user gets in at the current share price - int256 vconRewards = _harvestRewards(msg.sender, destination); /// auto-compound rewards + int256 vconRewards = _updateUserProfitIndex(msg.sender, destination); /// auto-compound rewards require( vconRewards >= 0, "MarketGovernance: must realize loss before staking" @@ -148,7 +149,7 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { /// amount of PCV to withdraw is the amount vcon * venue balance / total vcon staked on venue uint256 amountPcv = getProRataPCVAmounts(source, amountVcon); - int256 vconRewards = _harvestRewards(msg.sender, source); /// pay msg.sender their rewards + int256 vconRewards = _updateUserProfitIndex(msg.sender, source); /// pay msg.sender their rewards uint256 vconReward = vconRewards.toUint256(); /// pay msg.sender their rewards require( @@ -237,7 +238,7 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { _accrue(venue); /// updates the venueUserStartingProfit mapping - int256 pnl = _harvestRewards(msg.sender, venue); + int256 pnl = _updateUserProfitIndex(msg.sender, venue); if (pnl < 0) { uint256 lossAmount; /// loss scenarios @@ -308,7 +309,8 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { /// 2. get expected amount of pcv in the venue based on the vcon staked in that venue and the total pcv /// b = a * total pcv /// - /// 3. find the delta in basis points between the expected amount of pcv in the venue vs the actual amount in the venue + /// 3. find the delta in percentage terms, scaled by 1 ether between the expected amount of pcv in the + /// venue vs the actual amount in the venue /// d = (a - b) / a /// /// @param venue to query @@ -372,13 +374,20 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { uint256 venuePcv = IPCVDeposit(venue).balance(); uint256 cachedVconStaked = venueVconDeposited[venue]; - console.log("cachedVconStaked: ", cachedVconStaked); - /// 0 checks as any 0 denominator will cause a revert if (cachedVconStaked == 0) { return 0; /// perfectly balanced at 0 PCV or 0 VCON staked } + /// @audit we do not add 1 to the pro rata PCV here. This means a withdrawal of 1 Wei of VCON + /// will allow removing a user's VCON without having to withdraw from a venue. + /// This is a known issue, however it is not harmful as it would require a quintillion withdrawals + /// to withdraw 1 VCON, which would cost at minimum 21,000e18 gas per withdraw, meaning it would cost at least 1 million ether + /// (likely more) to retrieve a single VCON without moving PCV. + /// the only reason this would ever get expoited is if a loss was taken and a user was trying to avoid realizing their portion + /// of the losses. However, in a loss scenario, the unstake function does not allow execution if the user has an unrealized + /// loss in that venue. This condition stops the aforementioned exploit. + /// fix would require rounding up in the protocol's favor, so that a withdrawal of 1 Wei of VCON has an actual withdraw amount uint256 proRataPcv = (amountVcon * venuePcv) / cachedVconStaked; return proRataPcv; @@ -566,7 +575,7 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { /// updates their venue starting profit index /// does not update how much VCON a user has staked to save on gas /// that updating happens in the calling function - function _harvestRewards( + function _updateUserProfitIndex( address user, address venue ) private returns (int256) { @@ -574,7 +583,6 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { /// get pending rewards int256 pendingRewardBalance = getPendingRewards(user, venue); - console.log("got pending rewards", pendingRewardBalance.toUint256()); /// then set the vcon current index for this user venueUserStartingProfit[venue][user] = currentProfitIndex; From 395f8c7b3c23ea85c99acaed4ec0af775e498657 Mon Sep 17 00:00:00 2001 From: Elliot Date: Thu, 26 Jan 2023 15:22:37 -0800 Subject: [PATCH 39/74] add additional test cases --- test/unit/vcon/MarketGovernance.t.sol | 135 ++++++++++++++++++-------- 1 file changed, 93 insertions(+), 42 deletions(-) diff --git a/test/unit/vcon/MarketGovernance.t.sol b/test/unit/vcon/MarketGovernance.t.sol index 0ae53f929..8566be310 100644 --- a/test/unit/vcon/MarketGovernance.t.sol +++ b/test/unit/vcon/MarketGovernance.t.sol @@ -15,8 +15,8 @@ contract UnitTestMarketGovernance is SystemUnitTest { MarketGovernance public mgov; MockPCVSwapper public pcvSwapper; - address public venue = address(10); /// a in hex - uint256 public profitToVconRatio = 5; /// for each 1 wei in profit, 5 wei of vcon is received + uint256 public profitToVconRatioUsdc = 5e12; /// for each 1 wei in profit, 5e12 wei of vcon is received + uint256 public profitToVconRatioDai = 5; /// for each 1 wei in profit, 5 wei of vcon is received uint256 public daiDepositAmount = 1_000_000e18; uint256 public usdcDepositAmount = 1_000_000e6; @@ -24,6 +24,19 @@ contract UnitTestMarketGovernance is SystemUnitTest { address userOne = address(1000); address userTwo = address(1001); + /// @notice emitted when profit to vcon ratio is updated + event ProfitToVconRatioUpdated( + address indexed venue, + uint256 oldRatio, + uint256 newRatio + ); + + /// @notice emitted when the router is updated + event PCVRouterUpdated( + address indexed oldPcvRouter, + address indexed newPcvRouter + ); + function setUp() public override { super.setUp(); @@ -40,7 +53,11 @@ contract UnitTestMarketGovernance is SystemUnitTest { vm.startPrank(addresses.governorAddress); - mgov.setProfitToVconRatio(venue, profitToVconRatio); + mgov.setProfitToVconRatio(address(pcvDepositDai), profitToVconRatioDai); + mgov.setProfitToVconRatio( + address(pcvDepositUsdc), + profitToVconRatioUsdc + ); core.grantPCVController(address(mgov)); core.grantLocker(address(mgov)); @@ -57,7 +74,7 @@ contract UnitTestMarketGovernance is SystemUnitTest { } function _initializeVenues() private { - pcvDepositUsdc.setLastRecordedProfit(10_000e18); + pcvDepositUsdc.setLastRecordedProfit(10_000e6); pcvDepositDai.setLastRecordedProfit(10_000e18); } @@ -65,7 +82,14 @@ contract UnitTestMarketGovernance is SystemUnitTest { assertEq(address(mgov.core()), coreAddress); assertEq(mgov.profitToVconRatio(address(0)), 0); - assertEq(mgov.profitToVconRatio(venue), profitToVconRatio); + assertEq( + mgov.profitToVconRatio(address(pcvDepositDai)), + profitToVconRatioDai + ); + assertEq( + mgov.profitToVconRatio(address(pcvDepositUsdc)), + profitToVconRatioUsdc + ); assertTrue(core.isLocker(address(mgov))); assertTrue(core.isPCVController(address(mgov))); @@ -241,40 +265,31 @@ contract UnitTestMarketGovernance is SystemUnitTest { ); /// overweight DAI } - // function testUserDepositsFailsNotInitialized() public { - // address user = address(1001); - // uint120 vconAmount = 1e18; - - // vm.startPrank(user); - // vcon.mint(user, vconAmount); - // vcon.approve(address(mgov), vconAmount); - // mgov.stake( - // vconAmount, - // 0, - // address(pcvDepositDai), - // address(pcvDepositUsdc), - // address(pcvSwapper) - // ); - // vm.stopPrank(); - - // vm.expectRevert("MarketGovernance: venue not initialized"); - // mgov.stake( - // vconAmount, - // 1e18, - // address(pcvDepositUsdc), - // address(pcvDepositDai), - // address(pcvSwapper) - // ); - - // vm.expectRevert("MarketGovernance: venue not initialized"); - // mgov.stake( - // vconAmount, - // 0, /// deposit no pcv, meaning things will be imbalanced - // address(pcvDepositUsdc), - // address(pcvDepositDai), - // address(pcvSwapper) - // ); - // } + function testUnstakingOneUserOneWei() public { + testSystemOneUser(); + + assertEq(vcon.balanceOf(address(this)), 0); + /// subtract 1 to counter the addition the protocol does + uint256 startingVconAmount = mgov.venueUserDepositedVcon( + address(pcvDepositUsdc), + address(this) + ); + + mgov.unstake( + startingVconAmount, + address(pcvDepositUsdc), + address(pcvDepositDai), + address(pcvSwapper), + address(this) + ); + + assertEq(vcon.balanceOf(address(this)), startingVconAmount); + + assertEq( + 0, + mgov.venueUserDepositedVcon(address(pcvDepositUsdc), address(this)) + ); + } function testUnstakingOneUser() public { testSystemOneUser(); @@ -335,8 +350,6 @@ contract UnitTestMarketGovernance is SystemUnitTest { assertEq(0, pcvOracle.getVenueBalance(address(pcvDepositUsdc))); assertEq(0, mgov.venueVconDeposited(address(pcvDepositUsdc))); - - // assertEq(vconAmount - startingVconStakedAmount, mgov.vconStaked()); } /// test withdrawing when src and dest are equal @@ -353,8 +366,46 @@ contract UnitTestMarketGovernance is SystemUnitTest { ); } + function testWithdrawWithProfits() public { + uint120 vconAmount = 1000e18; + testSystemThreeUsersLastNoDeposit(vconAmount); + + pcvDepositUsdc.setLastRecordedProfit(20_000e6); + pcvDepositDai.setLastRecordedProfit(20_000e18); + + /// how much VCON is owed? + uint256 vconOwedUsdcRewards = 10_000e6 * profitToVconRatioUsdc; + uint256 vconOwedDaiRewards = 10_000e18 * profitToVconRatioDai; + + uint256 totalVconRewardsOwed = vconOwedUsdcRewards + vconOwedDaiRewards; + vcon.mint(address(mgov), totalVconRewardsOwed); + } + + function testSetProfitToVconRatio(address venue, uint256 ratio) public { + uint256 oldProfitToVconRatio = mgov.profitToVconRatio(venue); + + vm.expectEmit(true, true, false, true, address(mgov)); + emit ProfitToVconRatioUpdated(venue, oldProfitToVconRatio, ratio); + + vm.prank(addresses.governorAddress); + mgov.setProfitToVconRatio(venue, ratio); + + assertEq(mgov.profitToVconRatio(venue), ratio); + } + + function testSetPCVRouter(address newRouter) public { + address oldPCVRouter = mgov.pcvRouter(); + + vm.expectEmit(true, true, false, true, address(mgov)); + emit PCVRouterUpdated(oldPCVRouter, newRouter); + + vm.prank(addresses.governorAddress); + mgov.setPCVRouter(newRouter); + + assertEq(mgov.pcvRouter(), newRouter); + } + /// todo test withdrawing - /// todo test depositing fails when venue not initialized /// todo test withdrawing when there are profits /// todo test withdrawing when there are losses From e89413812ca7cdc889c54b482a4bcaacf75b9f8e Mon Sep 17 00:00:00 2001 From: Elliot Date: Fri, 27 Jan 2023 00:23:18 -0800 Subject: [PATCH 40/74] Harvest event update --- src/vcon/IMarketGovernance.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vcon/IMarketGovernance.sol b/src/vcon/IMarketGovernance.sol index 10b052faa..952c5a815 100644 --- a/src/vcon/IMarketGovernance.sol +++ b/src/vcon/IMarketGovernance.sol @@ -66,10 +66,11 @@ interface IMarketGovernance { ); /// @notice emitted whenever a user harvests + /// vcon will be negative if a loss is realized event Harvest( address indexed venue, address indexed user, - uint256 vconAmount + int256 vconAmount ); /// @notice emitted when a venue's index is updated via accrue From 405729e9ae1aecefa39facfeb453752f17baedf6 Mon Sep 17 00:00:00 2001 From: Elliot Date: Fri, 27 Jan 2023 00:24:26 -0800 Subject: [PATCH 41/74] add venueUserVconStartingSharePrice to Market Governance --- src/vcon/MarketGovernance.sol | 117 ++++++++++++++++++++++++++-------- 1 file changed, 91 insertions(+), 26 deletions(-) diff --git a/src/vcon/MarketGovernance.sol b/src/vcon/MarketGovernance.sol index d3d243f36..0e11246b8 100644 --- a/src/vcon/MarketGovernance.sol +++ b/src/vcon/MarketGovernance.sol @@ -13,9 +13,11 @@ import {IPCVDepositV2} from "@voltprotocol/pcv/IPCVDepositV2.sol"; import {IMarketGovernance} from "@voltprotocol/vcon/IMarketGovernance.sol"; import {DeviationWeiGranularity} from "@voltprotocol/utils/DeviationWeiGranularity.sol"; +import {console} from "@forge-std/console.sol"; + /// @notice this contract requires the PCV Mover and Locker role /// -/// Core formula for market governance rewards: +/// Formula for market governance rewards share price: /// Profit Per VCON = Profit Per VCON + ∆Cumulative Profits (Dollars) * VCON:Dollar / VCON Staked /// /// If an account has an unrealized loss on a venue, they cannot do any other action on that venue @@ -28,6 +30,16 @@ import {DeviationWeiGranularity} from "@voltprotocol/utils/DeviationWeiGranulari /// /// @dev this contract assumes it is already topped up with the VCON necessary to pay rewards. /// A dripper will keep this contract funded at a steady pace. +/// +/// Issues I'm seeing so far +/// 1. if a loss occurs in a venue, then everyone's VCON balance gets marked down. +/// However, mark downs only occur when a loss is realized. This means unstaking after you have first realized +/// a loss and someone else hasn't, your pro-rata portion will be less, because the amount of VCON you have staked +/// has gone down. +/// 2. if a gain occurs, everyone's VCON balance marks up. +/// However, mark ups only occur when a gain is realized. This means unstaking after you have realized a gain and +/// someone else hasn't, your pro-rata portion of the PCV will be more than it would be if everyone had realized +/// their gains. contract MarketGovernance is CoreRefV2, IMarketGovernance { using DeviationWeiGranularity for *; using SafeERC20 for *; @@ -53,6 +65,9 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { /// @notice last recorded profit index per venue mapping(address => uint128) public venueLastRecordedProfit; + /// @notice last recorded VCON share price index per venue + mapping(address => uint128) public venueLastRecordedVconSharePrice; + /// @notice total vcon deposited per venue mapping(address => uint256) public venueVconDeposited; @@ -62,7 +77,7 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { /// @notice record of VCON index when user joined a given venue mapping(address => mapping(address => uint256)) - public venueUserStartingProfit; + public venueUserVconStartingSharePrice; /// @notice record how much VCON a user deposited in a given venue mapping(address => mapping(address => uint256)) @@ -77,11 +92,21 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { /// TODO update pcv oracle to cache total pcv so getAllPCV isn't needed to figure /// out if weights are correct + /// @notice update the VCON share price and the last recorded profit for a given venue + /// @param venue address to accrue + function accrueVcon(address venue) external globalLock(1) { + IPCVOracle oracle = pcvOracle(); + + require(oracle.isVenue(venue), "MarketGovernance: invalid destination"); + + _accrue(venue); + } + /// ---------- Permissionless User PCV Allocation Methods ---------- /// @notice a user can get slashed up to their full VCON stake for entering /// a venue that takes a loss. - /// any losses or gains are applied to venueUserStartingProfit via `_accrue` method + /// any losses or gains are applied to venueUserVconStartingSharePrice via `_accrue` method /// @param amountVcon to stake on destination /// @param destination address to accrue rewards to, and send funds to function stake( @@ -237,7 +262,7 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { /// updates the venueLastRecordedProfit _accrue(venue); - /// updates the venueUserStartingProfit mapping + /// updates the venueUserVconStartingSharePrice mapping int256 pnl = _updateUserProfitIndex(msg.sender, venue); if (pnl < 0) { uint256 lossAmount; @@ -280,7 +305,7 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { /// gain or even scenario totalVcon += pnl.toUint256(); if (pnl != 0) { - emit Harvest(venue, msg.sender, pnl.toUint256()); + emit Harvest(venue, msg.sender, pnl); } } @@ -380,11 +405,11 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { } /// @audit we do not add 1 to the pro rata PCV here. This means a withdrawal of 1 Wei of VCON - /// will allow removing a user's VCON without having to withdraw from a venue. + /// will allow removing a user's VCON without having to withdraw PCV from a venue. /// This is a known issue, however it is not harmful as it would require a quintillion withdrawals - /// to withdraw 1 VCON, which would cost at minimum 21,000e18 gas per withdraw, meaning it would cost at least 1 million ether + /// to withdraw 1 VCON, which would cost at minimum 5,000e18 gas per withdraw, meaning it would cost at least 1 million ether /// (likely more) to retrieve a single VCON without moving PCV. - /// the only reason this would ever get expoited is if a loss was taken and a user was trying to avoid realizing their portion + /// The only reason this would ever get expoited is if a loss was taken and a user was trying to avoid realizing their portion /// of the losses. However, in a loss scenario, the unstake function does not allow execution if the user has an unrealized /// loss in that venue. This condition stops the aforementioned exploit. /// fix would require rounding up in the protocol's favor, so that a withdrawal of 1 Wei of VCON has an actual withdraw amount @@ -464,23 +489,28 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { address user, address venue ) public view returns (int256) { - /// TODO this is 5 SLOAD's and we can do better - /// Pack venue info down into a single struct to get us down to 3 SLOAD's + /// get user starting share price + int256 startingVconSharePrice = venueUserVconStartingSharePrice[venue][ + user + ].toInt256(); - int256 startingProfitIndex = venueUserStartingProfit[venue][user] + /// get venue current share price + int256 currentVconSharePrice = venueLastRecordedVconSharePrice[venue] .toInt256(); - int256 currentProfitIndex = venueLastRecordedProfit[venue].toInt256(); - int256 profitToVcon = profitToVconRatio[venue].toInt256(); + + /// get venue vcon amount staked int256 venueVconAmount = venueVconDeposited[venue].toInt256(); - if (startingProfitIndex == 0 || venueVconAmount == 0) { + if (startingVconSharePrice == 0 || venueVconAmount == 0) { return 0; /// no interest if user has not entered the market } - int256 currentProfits = currentProfitIndex - startingProfitIndex; - int256 vconRewards = (currentProfits * - venueUserDepositedVcon[venue][user].toInt256() * - profitToVcon) / venueVconAmount; + int256 userCurrentProfitPerVcon = currentVconSharePrice - + startingVconSharePrice; + + int256 vconRewards = (userCurrentProfitPerVcon * + venueUserDepositedVcon[venue][user].toInt256()) / + Constants.ETH_GRANULARITY_INT; return vconRewards; } @@ -579,29 +609,66 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { address user, address venue ) private returns (int256) { - uint256 currentProfitIndex = venueLastRecordedProfit[venue]; + uint256 currentVenueSharePrice = venueLastRecordedVconSharePrice[venue]; /// get pending rewards int256 pendingRewardBalance = getPendingRewards(user, venue); - /// then set the vcon current index for this user - venueUserStartingProfit[venue][user] = currentProfitIndex; + /// then set the vcon share price for this user + venueUserVconStartingSharePrice[venue][user] = currentVenueSharePrice; - /// if there are losses, revert on SafeCast - emit Harvest(venue, user, pendingRewardBalance.toUint256()); + /// emit harvest if there are gains or losses + emit Harvest(venue, user, pendingRewardBalance); return pendingRewardBalance; } - /// update the venue last recorded share price + /// update the venue last recorded profit + /// and the venue last recorded vcon share price function _accrue(address venue) private { IPCVDepositV2(venue).accrue(); + uint256 startingLastRecordedProfit = venueLastRecordedProfit[venue]; uint256 endingLastRecordedProfit = IPCVDepositV2(venue) .lastRecordedProfit(); + uint256 endingLastRecordedSharePrice = venueLastRecordedVconSharePrice[ + venue + ]; /// update venue last recorded profit regardless /// of participation in market governance + if (endingLastRecordedSharePrice != 0) { + uint256 venueStakedVcon = venueVconDeposited[venue]; + uint256 venueProfitRatio = profitToVconRatio[venue]; + + /// if venue has 0 staked vcon, do not update share price, just update profit index + if (venueStakedVcon != 0) { + int256 venueProfit = (endingLastRecordedProfit.toInt256() - + startingLastRecordedProfit.toInt256()); + int256 vconEarnedPerShare = (Constants.ETH_GRANULARITY_INT * + venueProfit * + venueProfitRatio.toInt256()) / venueStakedVcon.toInt256(); + + if (vconEarnedPerShare >= 0) { + /// gain scenario + venueLastRecordedVconSharePrice[venue] += ( + vconEarnedPerShare.toUint256() + ).toUint128(); + } else { + /// loss scenario + /// turn losses positive and subtract them + venueLastRecordedVconSharePrice[venue] -= ( + -vconEarnedPerShare + ).toUint256().toUint128(); + } + } + } else { + /// share price is 0, meaning it is not initialized, so initialize + venueLastRecordedVconSharePrice[venue] = Constants + .ETH_GRANULARITY + .toUint128(); + } + venueLastRecordedProfit[venue] = endingLastRecordedProfit.toUint128(); emit VenueIndexUpdated( @@ -611,8 +678,6 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { ); } - /// TODO add events to all functions - /// ---------- Governor-Only Permissioned API ---------- function setProfitToVconRatio( From dd54646a68ff6a414ef2f61dd1bf8d601c532360 Mon Sep 17 00:00:00 2001 From: Elliot Date: Fri, 27 Jan 2023 00:24:44 -0800 Subject: [PATCH 42/74] remove console.log --- test/mock/MockPCVSwapper.sol | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/test/mock/MockPCVSwapper.sol b/test/mock/MockPCVSwapper.sol index 5a6747763..a8a2ddd40 100644 --- a/test/mock/MockPCVSwapper.sol +++ b/test/mock/MockPCVSwapper.sol @@ -1,7 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.13; -import {console} from "@forge-std/console.sol"; - import {MockERC20} from "@test/mock/MockERC20.sol"; import {IPCVSwapper} from "@voltprotocol/pcv/IPCVSwapper.sol"; @@ -42,9 +41,6 @@ contract MockPCVSwapper is IPCVSwapper { ? (amountIn * exchangeRate) / 1e18 : ((amountIn * 1e18) / exchangeRate); - console.log("amountIn: ", amountIn); - console.log("amountOut: ", amountOut); - MockERC20(assetIn).mockBurn(address(this), amountIn); MockERC20(assetOut).mint(destination, amountOut); From f0cd71f30024b0f31bfdf51aac7e8af006767313 Mon Sep 17 00:00:00 2001 From: Elliot Date: Fri, 27 Jan 2023 00:26:09 -0800 Subject: [PATCH 43/74] add realizeGainsAndLosses tests --- test/unit/vcon/MarketGovernance.t.sol | 107 +++++++++++++++++++++++++- 1 file changed, 106 insertions(+), 1 deletion(-) diff --git a/test/unit/vcon/MarketGovernance.t.sol b/test/unit/vcon/MarketGovernance.t.sol index 8566be310..6bf264dc2 100644 --- a/test/unit/vcon/MarketGovernance.t.sol +++ b/test/unit/vcon/MarketGovernance.t.sol @@ -1,5 +1,7 @@ pragma solidity 0.8.13; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; + import {console} from "@forge-std/console.sol"; import {MockERC20} from "@test/mock/MockERC20.sol"; import {VoltRoles} from "@voltprotocol/core/VoltRoles.sol"; @@ -12,6 +14,8 @@ import {IMarketGovernance} from "@voltprotocol/vcon/IMarketGovernance.sol"; import {TestAddresses as addresses} from "@test/unit/utils/TestAddresses.sol"; contract UnitTestMarketGovernance is SystemUnitTest { + using SafeCast for *; + MarketGovernance public mgov; MockPCVSwapper public pcvSwapper; @@ -366,7 +370,7 @@ contract UnitTestMarketGovernance is SystemUnitTest { ); } - function testWithdrawWithProfits() public { + function testWithdrawWithGains() public { uint120 vconAmount = 1000e18; testSystemThreeUsersLastNoDeposit(vconAmount); @@ -379,6 +383,68 @@ contract UnitTestMarketGovernance is SystemUnitTest { uint256 totalVconRewardsOwed = vconOwedUsdcRewards + vconOwedDaiRewards; vcon.mint(address(mgov), totalVconRewardsOwed); + + mgov.accrueVcon(address(pcvDepositUsdc)); + + uint256 startingVconBalance = vcon.balanceOf(address(this)); + int256 pendingRewards = mgov.getPendingRewards( + address(this), + address(pcvDepositUsdc) + ); + + address[] memory venues = new address[](1); + venues[0] = address(pcvDepositUsdc); + mgov.realizeGainsAndLosses(venues); + + uint256 endingVconBalance = vcon.balanceOf(address(this)); + + assertEq( + pendingRewards, + endingVconBalance.toInt256() - startingVconBalance.toInt256() + ); + assertEq( + mgov.getPendingRewards(address(this), address(pcvDepositUsdc)), + 0 + ); + } + + function testWithdrawWithLosses() public { + uint120 vconAmount = 1000e18; + testSystemThreeUsersLastNoDeposit(vconAmount); + + pcvDepositUsdc.setLastRecordedProfit(0); /// all profits are lost usdc venue + pcvDepositDai.setLastRecordedProfit(0); /// all profits are lost dai venue + + mgov.accrueVcon(address(pcvDepositUsdc)); + + uint256 startingVconBalance = mgov.venueUserDepositedVcon( + address(pcvDepositUsdc), + address(this) + ); + int256 pendingRewards = mgov.getPendingRewards( + address(this), + address(pcvDepositUsdc) + ); + + address[] memory venues = new address[](1); + venues[0] = address(pcvDepositUsdc); + mgov.realizeGainsAndLosses(venues); + + uint256 endingVconBalance = mgov.venueUserDepositedVcon( + address(pcvDepositUsdc), + address(this) + ); + + assertEq( + pendingRewards, + endingVconBalance.toInt256() - startingVconBalance.toInt256() + ); + assertEq( + mgov + .getPendingRewards(address(this), address(pcvDepositUsdc)) + .toUint256(), + 0 + ); } function testSetProfitToVconRatio(address venue, uint256 ratio) public { @@ -409,6 +475,45 @@ contract UnitTestMarketGovernance is SystemUnitTest { /// todo test withdrawing when there are profits /// todo test withdrawing when there are losses + /// todo test stake, unstake, rebalance, accrueVcon, realize gains + /// and losses with invalid venues to ensure reverts + function testStakeInvalidVenueFails() public { + vm.expectRevert("MarketGovernance: invalid destination"); + mgov.stake(0, address(0)); + } + + function testAccrueInvalidVenueFails() public { + vm.expectRevert("MarketGovernance: invalid destination"); + mgov.accrueVcon(address(0)); + } + + function testUnstakeInvalidSourceVenueFails() public { + vm.expectRevert("MarketGovernance: invalid source"); + mgov.unstake(0, address(0), address(0), address(0), address(0)); + } + + function testUnstakeInvalidDestinationVenueFails() public { + vm.expectRevert("MarketGovernance: invalid destination"); + mgov.unstake( + 0, + address(pcvDepositDai), + address(0), + address(0), + address(0) + ); + } + + function testUnstakeSrcAndDestVenueEqualFails() public { + vm.expectRevert("MarketGovernance: src and dest equal"); + mgov.unstake( + 0, + address(pcvDepositDai), + address(pcvDepositDai), + address(0), + address(0) + ); + } + function testSetProfitToVconRatioFailsNonGovernor() public { vm.expectRevert("CoreRef: Caller is not a governor"); mgov.setProfitToVconRatio(address(0), 0); From 6920af7cd794b6ecc87526925385d809da5d42c1 Mon Sep 17 00:00:00 2001 From: Elliot Date: Fri, 27 Jan 2023 01:00:09 -0800 Subject: [PATCH 44/74] natspec --- src/vcon/MarketGovernance.sol | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/vcon/MarketGovernance.sol b/src/vcon/MarketGovernance.sol index 0e11246b8..05be33360 100644 --- a/src/vcon/MarketGovernance.sol +++ b/src/vcon/MarketGovernance.sol @@ -17,9 +17,6 @@ import {console} from "@forge-std/console.sol"; /// @notice this contract requires the PCV Mover and Locker role /// -/// Formula for market governance rewards share price: -/// Profit Per VCON = Profit Per VCON + ∆Cumulative Profits (Dollars) * VCON:Dollar / VCON Staked -/// /// If an account has an unrealized loss on a venue, they cannot do any other action on that venue /// until they have called the function realizeLosses and marked down the amount of VCON they have staked /// on that venue. Once the loss has been marked down, they can proceed with other actions. @@ -31,6 +28,23 @@ import {console} from "@forge-std/console.sol"; /// @dev this contract assumes it is already topped up with the VCON necessary to pay rewards. /// A dripper will keep this contract funded at a steady pace. /// +/// three main data points are tracked in each venue. +/// 1. the last recorded profit in a given venue. this tracks the last recorded profit amount in the underlying venue. +/// 2. the vcon share price in a given venue. this applies the last recorded profit across all VCON stakers in the +/// venue evenly and ensures that each user receives their pro-rata share of rewards. +/// 3. profit to vcon ratio in each venue. this tracks the profit to vcon ratio for each venue and is used to calculate +/// the vcon share price by finding the new last recorded profit, and finding the profit delta +/// vcon share price formula +/// +/// Formula for market governance rewards share price: +/// ∆Cumulative Profits (Dollars) = currentProfits - lastRecordedProfits +/// Profit Per VCON = Profit Per VCON + ∆Cumulative Profits (Dollars) * VCON:Dollar Ratio / Venue VCON Staked +/// +/// Formula for calculating user VCON rewards: +/// User VCON rewards = (Profit Per VCON - User Starting Profit Per VCON) * VCON Staked +/// +/// Anytime the rewards share price changes, so does the unclaimed user VCON rewards. +/// /// Issues I'm seeing so far /// 1. if a loss occurs in a venue, then everyone's VCON balance gets marked down. /// However, mark downs only occur when a loss is realized. This means unstaking after you have first realized From f9f61c9f0182d25cd9344764954abad13307c69f Mon Sep 17 00:00:00 2001 From: Elliot Date: Fri, 27 Jan 2023 14:48:55 -0800 Subject: [PATCH 45/74] system arch refactor to use share prices for each venue --- src/vcon/IMarketGovernance.sol | 22 +- src/vcon/MarketGovernance.sol | 486 +++++++++----------------- test/unit/vcon/MarketGovernance.t.sol | 162 +++++---- 3 files changed, 259 insertions(+), 411 deletions(-) diff --git a/src/vcon/IMarketGovernance.sol b/src/vcon/IMarketGovernance.sol index 952c5a815..55236a074 100644 --- a/src/vcon/IMarketGovernance.sol +++ b/src/vcon/IMarketGovernance.sol @@ -14,6 +14,11 @@ interface IMarketGovernance { uint256 amountPcv; } + struct PCVDepositInfo { + address deposit; + uint256 amount; + } + /// ---------- Events ---------- /// @notice event emitted when a user stakes their VCON @@ -109,21 +114,4 @@ interface IMarketGovernance { /// causing the entire transaction to fail. /// @param movements of PCV between venues function rebalance(Rebalance[] calldata movements) external; - - /// @notice realize gains and losses for msg.sender - /// @param venues to realize losses in - /// only the caller can realize losses on their own behalf - /// duplicating addresses does not allow theft as all venues have their indexes - /// updated before we find the profit and loss, so a duplicate venue will have 0 delta a second time - /// @dev we can't follow CEI here because we have to make external calls to update - /// the external venues. However, this is not an issue as the global reentrancy lock is enabled - function realizeGainsAndLosses(address[] calldata venues) external; - - /// return the total amount of rewards a user is entitled to - /// this value will usually be stale as .accrue() must be called in the same block/tx as this function - /// for it to return the proper amount of profit - function getAccruedRewards( - address[] calldata venues, - address user - ) external view returns (int256); } diff --git a/src/vcon/MarketGovernance.sol b/src/vcon/MarketGovernance.sol index 05be33360..e72aa2dce 100644 --- a/src/vcon/MarketGovernance.sol +++ b/src/vcon/MarketGovernance.sol @@ -62,9 +62,6 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { /// @notice reference to the PCV Router address public pcvRouter; - /// @notice total amount of VCON deposited across all venues - uint256 public vconStaked; - /// @dev convention for all normal mappings is key (venue -> value) /// @notice amount of VCON paid per unit of revenue generated per venue @@ -73,7 +70,7 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { mapping(address => uint256) public profitToVconRatio; /// and do the conversion to VCON at the end when accruing rewards - /// pack venueLastRecordedProfit, venueVconDeposited and profitToVconRatio + /// pack venueLastRecordedProfit, venueTotalShares and profitToVconRatio /// into a single slot for gas optimization /// @notice last recorded profit index per venue @@ -83,19 +80,14 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { mapping(address => uint128) public venueLastRecordedVconSharePrice; /// @notice total vcon deposited per venue - mapping(address => uint256) public venueVconDeposited; + mapping(address => uint256) public venueTotalShares; /// ---------- Per Venue User Profit Tracking ---------- /// @dev convention for all double nested address mappings is key (venue -> user) -> value - /// @notice record of VCON index when user joined a given venue - mapping(address => mapping(address => uint256)) - public venueUserVconStartingSharePrice; - /// @notice record how much VCON a user deposited in a given venue - mapping(address => mapping(address => uint256)) - public venueUserDepositedVcon; + mapping(address => mapping(address => uint256)) public venueUserShares; /// @param _core reference to core /// @param _pcvRouter reference to pcvRouter @@ -120,7 +112,6 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { /// @notice a user can get slashed up to their full VCON stake for entering /// a venue that takes a loss. - /// any losses or gains are applied to venueUserVconStartingSharePrice via `_accrue` method /// @param amountVcon to stake on destination /// @param destination address to accrue rewards to, and send funds to function stake( @@ -133,23 +124,17 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { "MarketGovernance: invalid destination" ); - _accrue(destination); /// update profitPerVCON in the destination so the user gets in at the current share price - int256 vconRewards = _updateUserProfitIndex(msg.sender, destination); /// auto-compound rewards - require( - vconRewards >= 0, - "MarketGovernance: must realize loss before staking" - ); - - uint256 totalVconDeposited = amountVcon + vconRewards.toUint256(); /// auto-compound rewards + _accrue(destination); /// update share price in the destination so the user gets in at the current share price - /// global updates - vconStaked += totalVconDeposited; + /// vconToShares will always return correctly as accrue will set venueLastRecordedVconSharePrice + /// to the correct share price from 0 if uninitialized + uint256 userShareAmount = vconToShares(destination, amountVcon); /// user updates - venueUserDepositedVcon[destination][msg.sender] += totalVconDeposited; + venueUserShares[destination][msg.sender] += userShareAmount; /// venue updates - venueVconDeposited[destination] += totalVconDeposited; + venueTotalShares[destination] += userShareAmount; /// check and an interaction with a trusted contract vcon().safeTransferFrom(msg.sender, address(this), amountVcon); /// transfer VCON in @@ -158,13 +143,13 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { } /// @notice unstake VCON and transfer corresponding VCON to another venue - /// @param amountVcon the amount of VCON staked to unstake + /// @param shareAmount the amount of shares to unstake /// @param source address to accrue rewards to, and pull funds from /// @param destination address to send funds /// @param swapper address to swap funds through /// @param vconRecipient address to receive the VCON function unstake( - uint256 amountVcon, + uint256 shareAmount, address source, address destination, address swapper, @@ -185,45 +170,61 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { _accrue(source); /// update profitPerVCON in the source so the user gets paid out at the current share price + /// figure out how balanced the system is before withdraw + /// amount of PCV to withdraw is the amount vcon * venue balance / total vcon staked on venue - uint256 amountPcv = getProRataPCVAmounts(source, amountVcon); + uint256 amountPcv = getProRataPCVAmounts(source, shareAmount); + uint256 amountVcon = sharesToVcon(source, shareAmount); - int256 vconRewards = _updateUserProfitIndex(msg.sender, source); /// pay msg.sender their rewards - uint256 vconReward = vconRewards.toUint256(); /// pay msg.sender their rewards + /// read unsafe because we are at lock level 1 + uint256 totalPcv = pcvOracle().getTotalPcvUnsafe(); - require( - vconRewards >= 0, - "MarketGovernance: must realize loss before unstaking" + /// record how balanced the system is before the PCV movement + uint256 totalVconStaked = getTotalVconStaked(); + + uint256 sourceExpectedPcv = getExpectedVenuePCVAmount( + source, + totalPcv, + totalVconStaked + ); + uint256 destinationExpectedPcv = getExpectedVenuePCVAmount( + source, + totalPcv, + totalVconStaked ); - require( - venueUserDepositedVcon[source][msg.sender] >= amountVcon, - "MarketGovernance: invalid vcon amount" + int256 sourceVenueBalance = getVenueDeviation( + source, + sourceExpectedPcv + ); + int256 destinationVenueBalance = getVenueDeviation( + destination, + destinationExpectedPcv ); - /// global updates - vconStaked -= amountVcon; + require( + venueUserShares[source][msg.sender] >= shareAmount, + "MarketGovernance: invalid share amount" + ); /// user updates - venueUserDepositedVcon[source][msg.sender] -= amountVcon; + venueUserShares[source][msg.sender] -= shareAmount; /// venue updates - venueVconDeposited[source] -= amountVcon; + venueTotalShares[source] -= shareAmount; /// ---------- Interactions ---------- - vcon().safeTransfer(vconRecipient, amountVcon + vconReward); /// transfer VCON amount + rewards to recipient - - /// ignore balance checks if only one user is in the system and is allocating to a single venue - bool ignoreBalanceChecks = vconStaked == - venueUserDepositedVcon[source][msg.sender]; + vcon().safeTransfer(vconRecipient, amountVcon); /// transfer VCON amount to recipient _movePCVWithChecks( source, destination, swapper, amountPcv, - ignoreBalanceChecks + totalPcv, + sourceVenueBalance, + destinationVenueBalance ); emit Unstaked(source, msg.sender, amountVcon, amountPcv); @@ -235,183 +236,113 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { /// @param movements information on all pcv movements /// including sources, destinations, amounts and swappers function rebalance(Rebalance[] calldata movements) external globalLock(1) { + /// read unsafe because we are at lock level 1 + uint256 totalPcv = pcvOracle().getTotalPcvUnsafe(); + unchecked { for (uint256 i = 0; i < movements.length; i++) { address source = movements[i].source; address destination = movements[i].destination; address swapper = movements[i].swapper; uint256 amountPcv = movements[i].amountPcv; + /// record how balanced the system is before the PCV movement + int256 sourceVenueBalance = getVenueDeviation(source, totalPcv); + int256 destinationVenueBalance = getVenueDeviation( + destination, + totalPcv + ); _movePCVWithChecks( source, destination, swapper, amountPcv, - false + totalPcv, + sourceVenueBalance, + destinationVenueBalance ); } } } - /// @notice realize gains and losses for msg.sender - /// @param venues to realize losses in - /// only the caller can realize losses on their own behalf - /// duplicating addresses does not allow theft as all venues have their indexes - /// updated before we find the profit and loss, so a duplicate venue will have 0 delta a second time - /// @dev we can't follow CEI here because we have to make external calls to update - /// the external venues. However, this is not an issue as the global reentrancy lock is enabled - function realizeGainsAndLosses( - address[] calldata venues - ) external globalLock(1) { - uint256 venueLength = venues.length; - uint256 totalVcon = 0; - - for (uint256 i = 0; i < venueLength; ) { - address venue = venues[i]; - require( - pcvOracle().isVenue(venue), - "MarketGovernance: invalid venue" - ); - - /// updates the venueLastRecordedProfit - _accrue(venue); - - /// updates the venueUserVconStartingSharePrice mapping - int256 pnl = _updateUserProfitIndex(msg.sender, venue); - if (pnl < 0) { - uint256 lossAmount; - /// loss scenarios - if ( - (-pnl).toUint256() > - venueUserDepositedVcon[venue][msg.sender] - ) { - uint256 userDepositedAmount = venueUserDepositedVcon[venue][ - msg.sender - ]; - /// zero the user's balance - venueUserDepositedVcon[venue][msg.sender] = 0; - - /// take losses off the total amount staked - vconStaked -= userDepositedAmount; - - /// final write to storage, decrement venue deposited VCON - venueVconDeposited[venue] -= userDepositedAmount; - - lossAmount = userDepositedAmount; - } else { - /// losses should never exceed staked amount at this point - venueUserDepositedVcon[venue][ - msg.sender - ] = (venueUserDepositedVcon[venue][msg.sender].toInt256() + - pnl).toUint256(); + /// ------------- View Only Methods ------------- + + /// @param venue to get share price from + /// @param shareAmount to withdraw + /// @return vconAmount the amount of VCON received for a given amount of shares + function sharesToVcon( + address venue, + uint256 shareAmount + ) public view returns (uint256 vconAmount) { + uint256 sharePrice = venueLastRecordedVconSharePrice[venue]; - /// take losses off the total amount staked - vconStaked = (vconStaked.toInt256() + pnl).toUint256(); + /// if share price is 0, accrueVcon must be called first + vconAmount = (sharePrice * shareAmount) / Constants.ETH_GRANULARITY; + } - /// decrement venue deposited VCON - venueVconDeposited[venue] -= (-pnl).toUint256(); + /// @param venue to get share price from + /// @param amountVcon to deposit + /// return the amount of shares received from depositing into a given venue + function vconToShares( + address venue, + uint256 amountVcon + ) public view returns (uint256 shareAmount) { + uint256 sharePrice = venueLastRecordedVconSharePrice[venue]; - lossAmount = (-pnl).toUint256(); - } + shareAmount = (amountVcon * Constants.ETH_GRANULARITY) / sharePrice; + } - emit LossRealized(venue, msg.sender, lossAmount); - } else { - /// gain or even scenario - totalVcon += pnl.toUint256(); - if (pnl != 0) { - emit Harvest(venue, msg.sender, pnl); - } - } + /// @notice returns the amount of VCON staked in a single venue + function getVenueVconStaked(address venue) public view returns (uint256) { + return + (venueTotalShares[venue] * venueLastRecordedVconSharePrice[venue]) / + Constants.ETH_GRANULARITY; + } + + /// get the total amount of VCON staked based on the last cached share prices of each venue + function getTotalVconStaked() + public + view + returns (uint256 totalVconStaked) + { + address[] memory pcvDeposits = pcvOracle().getVenues(); + uint256 totalVenues = pcvDeposits.length; + for (uint256 i = 0; i < totalVenues; ) { + address venue = pcvDeposits[i]; + + totalVconStaked += getVenueVconStaked(venue); unchecked { i++; } } - - if (totalVcon != 0) { - vcon().safeTransfer(msg.sender, totalVcon); - } } - /// ------------- View Only Methods ------------- - - /// @notice returns positive value if over allocated - /// returns negative value if under allocated + /// @notice returns positive value if under allocated + /// returns negative value if over allocated /// if no venue balance and deposited vcon, return positive /// if venue balance and no deposited vcon, return negative /// - /// algorithm for determining how balanced a venue is: - /// - /// 1. get ratio of how much vcon is deposited into the venue out of the total supply - /// a = venue deposited vcon / total vcon staked - /// - /// 2. get expected amount of pcv in the venue based on the vcon staked in that venue and the total pcv - /// b = a * total pcv - /// - /// 3. find the delta in percentage terms, scaled by 1 ether between the expected amount of pcv in the - /// venue vs the actual amount in the venue - /// d = (a - b) / a - /// /// @param venue to query - /// @param totalPcv to measure venue against + /// @param expectedVenueBalance expected venue pcv function getVenueDeviation( address venue, - uint256 totalPcv + uint256 expectedVenueBalance ) public view returns (int256) { - uint256 venueDepositedVcon = venueVconDeposited[venue]; uint256 venueBalance = pcvOracle().getVenueBalance(venue); - uint256 cachedVconStaked = vconStaked; - - /// 0 checks as any 0 denominator will cause a revert - if ( - (venueDepositedVcon == 0 && venueBalance == 0) || - cachedVconStaked == 0 || - totalPcv == 0 - ) { - return 0; /// perfectly balanced at 0 PCV or 0 VCON staked - } - /// Step 1. - /// find out actual ratio of VCON in a given venue based on total VCON staked - uint256 venueDepositedVconRatio = (venueDepositedVcon * - Constants.ETH_GRANULARITY) / cachedVconStaked; - - /// perfectly balanced - if (venueDepositedVconRatio == 0 && venueBalance == 0) { - return 0; - } - - if (venueDepositedVconRatio == 0) { - /// add this 0 check because deviation divides by a and would cause a revert - /// replicate step 3 if no VCON is deposited by comparing pcv to venue balance - return Constants.ETH_GRANULARITY_INT; - } - - /// Step 2. - /// get expected pcv amount - uint256 expectedPcvAmount = (venueDepositedVconRatio * totalPcv) / - Constants.ETH_GRANULARITY; - - /// perfectly balanced = (expectedPcvAmount - venueBalance) * 1e18 / expectedPcvAmount = 0 - - /// Step 3. - /// if venue deposited VCON, return the ratio between expected pcv vs actual pcv - return - -DeviationWeiGranularity.calculateDeviation( - expectedPcvAmount.toInt256(), - venueBalance.toInt256() - ); + return venueBalance.toInt256() - expectedVenueBalance.toInt256(); } /// @param venue to figure out total pro rata pcv /// @param amountVcon to find total amount of pro rata pcv - /// @return the pro rata pcv controll ed in the given venue based on the amount of VCON + /// @return the pro rata pcv controlled in the given venue based on the amount of VCON function getProRataPCVAmounts( address venue, uint256 amountVcon ) public view returns (uint256) { uint256 venuePcv = IPCVDeposit(venue).balance(); - uint256 cachedVconStaked = venueVconDeposited[venue]; + uint256 cachedVconStaked = sharesToVcon(venue, venueTotalShares[venue]); /// 0 checks as any 0 denominator will cause a revert if (cachedVconStaked == 0) { @@ -432,33 +363,7 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { return proRataPcv; } - /// @notice return the amount of rewards accrued so far - /// without calling accrue on the underlying venues - function getAccruedRewards( - address[] calldata venues, - address user - ) external view returns (int256 totalVcon) { - uint256 venueLength = venues.length; - - unchecked { - for (uint256 i = 0; i < venueLength; i++) { - address venue = venues[i]; - require( - pcvOracle().isVenue(venue), - "MarketGovernance: invalid venue" - ); - - totalVcon += getPendingRewards(user, venue); - } - } - } - - struct PCVDepositInfo { - address deposit; - uint256 amount; - } - - /// @notice return what the perfectly balanced system would look like + /// @notice return what the perfectly balanced system would look like with all balances normalized to 1e18 function getExpectedPCVAmounts() public view @@ -467,66 +372,38 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { address[] memory pcvDeposits = pcvOracle().getVenues(); uint256 totalVenues = pcvDeposits.length; uint256 totalPcv = pcvOracle().getTotalPcv(); - uint256 cachedVconStaked = vconStaked; /// Save repeated warm SLOADs + uint256 cachedVconStaked = getTotalVconStaked(); /// Save repeated warm SLOADs deposits = new PCVDepositInfo[](totalVenues); unchecked { for (uint256 i = 0; i < totalVenues; i++) { address venue = pcvDeposits[i]; - uint256 venueDepositedVcon = venueVconDeposited[venue]; deposits[i].deposit = venue; - - if (venueDepositedVcon == 0) { - deposits[i].amount = 0; - } else { - uint256 expectedPcvAmount = (venueDepositedVcon * - totalPcv) / cachedVconStaked; - - deposits[i].amount = expectedPcvAmount; - } + deposits[i].amount = getExpectedVenuePCVAmount( + venue, + totalPcv, + cachedVconStaked + ); } } } - /// @notice VCON rewards could be negative if a user is at a loss - /// @param user to check rewards from - /// @param venue to check rewards in - /// return the amount of pending rewards or losses for a given user - /// algorithm: - /// 1. Determine venue profits or losses that have occured since - /// the last time the user staked or unstaked in a given venue. - /// venue pnl = current profit index - user starting index - /// 2. Convert PnL to their pro-rata VCON amount - /// vcon rewards = venue PnL * user vcon deposited in venue / total vcon deposited in venue - function getPendingRewards( - address user, - address venue - ) public view returns (int256) { - /// get user starting share price - int256 startingVconSharePrice = venueUserVconStartingSharePrice[venue][ - user - ].toInt256(); - - /// get venue current share price - int256 currentVconSharePrice = venueLastRecordedVconSharePrice[venue] - .toInt256(); - - /// get venue vcon amount staked - int256 venueVconAmount = venueVconDeposited[venue].toInt256(); + function getExpectedVenuePCVAmount( + address venue, + uint256 totalPcv, + uint256 totalVconStaked + ) public view returns (uint256 expectedPcvAmount) { + uint256 venueDepositedVcon = sharesToVcon( + venue, + venueTotalShares[venue] + ); - if (startingVconSharePrice == 0 || venueVconAmount == 0) { - return 0; /// no interest if user has not entered the market + if (totalVconStaked == 0) { + return 0; } - int256 userCurrentProfitPerVcon = currentVconSharePrice - - startingVconSharePrice; - - int256 vconRewards = (userCurrentProfitPerVcon * - venueUserDepositedVcon[venue][user].toInt256()) / - Constants.ETH_GRANULARITY_INT; - - return vconRewards; + expectedPcvAmount = (venueDepositedVcon * totalPcv) / totalVconStaked; } /// ------------- Helper Methods ------------- @@ -535,30 +412,19 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { /// @param destination recipient address for funds /// @param swapper address to swap tokens with /// @param amountPcv the amount of PCV to move from source - /// @param ignoreBalanceChecks whether or not to ignore balance checks function _movePCVWithChecks( address source, address destination, address swapper, uint256 amountPcv, - bool ignoreBalanceChecks + uint256 totalPcv, + int256 sourceVenueBalance, + int256 destinationVenueBalance ) private { address sourceAsset = IPCVDepositV2(source).balanceReportedIn(); address destinationAsset = IPCVDepositV2(destination) .balanceReportedIn(); - /// read unsafe because we are at lock level 1 - uint256 totalPcv = pcvOracle().getTotalPcvUnsafe(); - - int256 sourceVenueBalance; - int256 destinationVenueBalance; - - if (!ignoreBalanceChecks) { - /// record how balanced the system is before the PCV movement - sourceVenueBalance = getVenueDeviation(source, totalPcv); /// 50% - destinationVenueBalance = getVenueDeviation(destination, totalPcv); /// -30% - } - /// validate pcv movement /// check underlying assets match up and if not that swapper is provided and valid PCVRouter(pcvRouter).movePCV( @@ -570,35 +436,33 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { destinationAsset ); - if (!ignoreBalanceChecks) { - /// TODO simplify this by passing delta of starting balance - /// instead of calling balance again on each pcv deposit - /// record how balanced the system is before the PCV movement - int256 sourceVenueBalanceAfter = getVenueDeviation( - source, - totalPcv - ); - int256 destinationVenueBalanceAfter = getVenueDeviation( - destination, - totalPcv - ); - - /// source and dest venue balance measures the distance from being perfectly balanced - - /// validate source venue balance became more balanced - _checkBalance( - sourceVenueBalance, - sourceVenueBalanceAfter, - "MarketGovernance: src more imbalanced" - ); - - /// validate destination venue balance became more balanced - _checkBalance( - destinationVenueBalance, - destinationVenueBalanceAfter, - "MarketGovernance: dest more imbalanced" - ); - } + /// TODO simplify this by passing delta of starting balance + /// instead of calling balance again on each pcv deposit + /// record how balanced the system is before the PCV movement + int256 sourceVenueBalanceAfter = getVenueDeviation(source, totalPcv); + int256 destinationVenueBalanceAfter = getVenueDeviation( + destination, + totalPcv + ); + + /// source and dest venue balance measures the distance from being perfectly balanced + + console.logInt(sourceVenueBalance); + console.logInt(sourceVenueBalanceAfter); + + /// validate source venue balance became more balanced + _checkBalance( + sourceVenueBalance, + sourceVenueBalanceAfter, + "MarketGovernance: src more imbalanced" + ); + + /// validate destination venue balance became more balanced + _checkBalance( + destinationVenueBalance, + destinationVenueBalanceAfter, + "MarketGovernance: dest more imbalanced" + ); } /// @notice helper function to validate balance moved in the right direction after a pcv movement @@ -609,40 +473,21 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { ) private pure { require( balanceBefore < 0 /// if balance is under weight relative to vcon staked, ensure it doesn't go over balance - ? balanceAfter > balanceBefore && balanceAfter <= 0 /// if balance is over weight relative to vcon staked, ensure it doesn't go under balance - : balanceAfter < balanceBefore && balanceAfter >= 0, + ? balanceAfter >= balanceBefore && balanceAfter <= 0 /// if balance is over weight relative to vcon staked, ensure it doesn't go under balance + : balanceAfter <= balanceBefore && balanceAfter >= 0, reason ); } - /// @notice returns profit or losses a VCON staker has accrued - /// updates their venue starting profit index - /// does not update how much VCON a user has staked to save on gas - /// that updating happens in the calling function - function _updateUserProfitIndex( - address user, - address venue - ) private returns (int256) { - uint256 currentVenueSharePrice = venueLastRecordedVconSharePrice[venue]; - - /// get pending rewards - int256 pendingRewardBalance = getPendingRewards(user, venue); - - /// then set the vcon share price for this user - venueUserVconStartingSharePrice[venue][user] = currentVenueSharePrice; - - /// emit harvest if there are gains or losses - emit Harvest(venue, user, pendingRewardBalance); - - return pendingRewardBalance; - } - /// update the venue last recorded profit /// and the venue last recorded vcon share price function _accrue(address venue) private { + /// cache starting recorded profit before the external call even though + /// there is no way to call _accrue without setting the global reentrancy lock to level 1 + uint256 startingLastRecordedProfit = venueLastRecordedProfit[venue]; + IPCVDepositV2(venue).accrue(); - uint256 startingLastRecordedProfit = venueLastRecordedProfit[venue]; uint256 endingLastRecordedProfit = IPCVDepositV2(venue) .lastRecordedProfit(); uint256 endingLastRecordedSharePrice = venueLastRecordedVconSharePrice[ @@ -652,16 +497,17 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { /// update venue last recorded profit regardless /// of participation in market governance if (endingLastRecordedSharePrice != 0) { - uint256 venueStakedVcon = venueVconDeposited[venue]; + uint256 venueShares = venueTotalShares[venue]; uint256 venueProfitRatio = profitToVconRatio[venue]; /// if venue has 0 staked vcon, do not update share price, just update profit index - if (venueStakedVcon != 0) { + if (venueShares != 0) { int256 venueProfit = (endingLastRecordedProfit.toInt256() - startingLastRecordedProfit.toInt256()); + int256 vconEarnedPerShare = (Constants.ETH_GRANULARITY_INT * venueProfit * - venueProfitRatio.toInt256()) / venueStakedVcon.toInt256(); + venueProfitRatio.toInt256()) / venueShares.toInt256(); if (vconEarnedPerShare >= 0) { /// gain scenario diff --git a/test/unit/vcon/MarketGovernance.t.sol b/test/unit/vcon/MarketGovernance.t.sol index 6bf264dc2..7d465438b 100644 --- a/test/unit/vcon/MarketGovernance.t.sol +++ b/test/unit/vcon/MarketGovernance.t.sol @@ -133,11 +133,11 @@ contract UnitTestMarketGovernance is SystemUnitTest { ); assertEq( - mgov.venueVconDeposited(address(pcvDepositUsdc)), + mgov.venueTotalShares(address(pcvDepositUsdc)), vconDepositAmount ); - assertEq(mgov.vconStaked(), vconDepositAmount); + assertEq(mgov.getTotalVconStaked(), vconDepositAmount); } function testSystemTwoUsers() public { @@ -179,8 +179,8 @@ contract UnitTestMarketGovernance is SystemUnitTest { vm.assume(vconAmount > 1e9); testSystemTwoUsers(); - uint256 startingTotalSupply = mgov.vconStaked(); - uint256 startingVconStaked = mgov.venueVconDeposited( + uint256 startingTotalSupply = mgov.getTotalVconStaked(); + uint256 startingVconStaked = mgov.venueTotalShares( address(pcvDepositUsdc) ); @@ -194,15 +194,15 @@ contract UnitTestMarketGovernance is SystemUnitTest { assertEq(vcon.balanceOf(userTwo), 0); assertEq( - mgov.venueVconDeposited(address(pcvDepositUsdc)), + mgov.venueTotalShares(address(pcvDepositUsdc)), vconAmount + startingVconStaked ); assertEq( - mgov.venueUserDepositedVcon(address(pcvDepositUsdc), userTwo), + mgov.venueUserShares(address(pcvDepositUsdc), userTwo), vconAmount ); - uint256 endingTotalSupply = mgov.vconStaked(); + uint256 endingTotalSupply = mgov.getTotalVconStaked(); uint256 totalPCV = pcvOracle.getTotalPcv(); assertEq(endingTotalSupply, startingTotalSupply + vconAmount); @@ -219,7 +219,7 @@ contract UnitTestMarketGovernance is SystemUnitTest { uint120 vconAmount = 1e9; address user = address(1001); - uint256 startingTotalSupply = mgov.vconStaked(); + uint256 startingTotalSupply = mgov.getTotalVconStaked(); vm.startPrank(user); @@ -229,7 +229,7 @@ contract UnitTestMarketGovernance is SystemUnitTest { vm.stopPrank(); - uint256 endingTotalSupply = mgov.vconStaked(); + uint256 endingTotalSupply = mgov.getTotalVconStaked(); uint256 totalPCV = pcvOracle.getTotalPcv(); assertEq(vcon.balanceOf(user), 0); @@ -248,7 +248,7 @@ contract UnitTestMarketGovernance is SystemUnitTest { _initializeVenues(); address user = address(1001); - uint256 startingTotalSupply = mgov.vconStaked(); + uint256 startingTotalSupply = mgov.getTotalVconStaked(); vm.startPrank(user); vcon.mint(user, vconAmount); @@ -256,7 +256,7 @@ contract UnitTestMarketGovernance is SystemUnitTest { mgov.stake(vconAmount, address(pcvDepositUsdc)); vm.stopPrank(); - uint256 endingTotalSupply = mgov.vconStaked(); + uint256 endingTotalSupply = mgov.getTotalVconStaked(); uint256 totalPCV = pcvOracle.getTotalPcv(); assertEq(vcon.balanceOf(user), 0); @@ -274,24 +274,24 @@ contract UnitTestMarketGovernance is SystemUnitTest { assertEq(vcon.balanceOf(address(this)), 0); /// subtract 1 to counter the addition the protocol does - uint256 startingVconAmount = mgov.venueUserDepositedVcon( + uint256 startingShareAmount = mgov.venueUserShares( address(pcvDepositUsdc), address(this) ); mgov.unstake( - startingVconAmount, + startingShareAmount, address(pcvDepositUsdc), address(pcvDepositDai), address(pcvSwapper), address(this) ); - assertEq(vcon.balanceOf(address(this)), startingVconAmount); + assertEq(vcon.balanceOf(address(this)), startingShareAmount); assertEq( 0, - mgov.venueUserDepositedVcon(address(pcvDepositUsdc), address(this)) + mgov.venueUserShares(address(pcvDepositUsdc), address(this)) ); } @@ -299,7 +299,7 @@ contract UnitTestMarketGovernance is SystemUnitTest { testSystemOneUser(); assertEq(vcon.balanceOf(address(this)), 0); - uint256 vconAmount = mgov.vconStaked(); + uint256 vconAmount = mgov.getTotalVconStaked(); mgov.unstake( vconAmount, @@ -316,12 +316,12 @@ contract UnitTestMarketGovernance is SystemUnitTest { pcvOracle.getTotalPcv(), pcvOracle.getVenueBalance(address(pcvDepositDai)) ); - assertEq(0, mgov.venueVconDeposited(address(pcvDepositDai))); + assertEq(0, mgov.venueTotalShares(address(pcvDepositDai))); assertEq(0, pcvOracle.getVenueBalance(address(pcvDepositUsdc))); - assertEq(0, mgov.venueVconDeposited(address(pcvDepositUsdc))); + assertEq(0, mgov.venueTotalShares(address(pcvDepositUsdc))); - assertEq(0, mgov.vconStaked()); + assertEq(0, mgov.getTotalVconStaked()); } function testUnstakingTwoUsers() public { @@ -329,8 +329,8 @@ contract UnitTestMarketGovernance is SystemUnitTest { assertEq(vcon.balanceOf(address(this)), 0); - uint256 startingVconStakedAmount = mgov.vconStaked(); - uint256 vconAmount = mgov.venueUserDepositedVcon( + uint256 startingVconStakedAmount = mgov.getTotalVconStaked(); + uint256 vconAmount = mgov.venueUserShares( address(pcvDepositUsdc), address(this) ); @@ -350,15 +350,18 @@ contract UnitTestMarketGovernance is SystemUnitTest { pcvOracle.getTotalPcv(), pcvOracle.getVenueBalance(address(pcvDepositDai)) ); - assertEq(startingVconStakedAmount - vconAmount, mgov.vconStaked()); + assertEq( + startingVconStakedAmount - vconAmount, + mgov.getTotalVconStaked() + ); assertEq(0, pcvOracle.getVenueBalance(address(pcvDepositUsdc))); - assertEq(0, mgov.venueVconDeposited(address(pcvDepositUsdc))); + assertEq(0, mgov.venueTotalShares(address(pcvDepositUsdc))); } /// test withdrawing when src and dest are equal function testWithdrawFailsSrcDestEqual() public { - uint256 vconAmount = mgov.vconStaked(); + uint256 vconAmount = mgov.getTotalVconStaked(); vm.expectRevert("MarketGovernance: src and dest equal"); mgov.unstake( @@ -374,6 +377,13 @@ contract UnitTestMarketGovernance is SystemUnitTest { uint120 vconAmount = 1000e18; testSystemThreeUsersLastNoDeposit(vconAmount); + IMarketGovernance.PCVDepositInfo[] memory info = mgov + .getExpectedPCVAmounts(); + for (uint256 i = 0; i < info.length; i++) { + console.log("deposit: ", info[i].deposit); + console.log("amount: ", info[i].amount); + } + pcvDepositUsdc.setLastRecordedProfit(20_000e6); pcvDepositDai.setLastRecordedProfit(20_000e18); @@ -384,69 +394,73 @@ contract UnitTestMarketGovernance is SystemUnitTest { uint256 totalVconRewardsOwed = vconOwedUsdcRewards + vconOwedDaiRewards; vcon.mint(address(mgov), totalVconRewardsOwed); - mgov.accrueVcon(address(pcvDepositUsdc)); - uint256 startingVconBalance = vcon.balanceOf(address(this)); - int256 pendingRewards = mgov.getPendingRewards( - address(this), - address(pcvDepositUsdc) + uint256 shareAmount = mgov.venueUserShares( + address(pcvDepositUsdc), + address(this) ); - address[] memory venues = new address[](1); - venues[0] = address(pcvDepositUsdc); - mgov.realizeGainsAndLosses(venues); - - uint256 endingVconBalance = vcon.balanceOf(address(this)); - - assertEq( - pendingRewards, - endingVconBalance.toInt256() - startingVconBalance.toInt256() + console.log( + "shares in venue USDC: ", + mgov.venueTotalShares(address(pcvDepositUsdc)) ); - assertEq( - mgov.getPendingRewards(address(this), address(pcvDepositUsdc)), - 0 + console.log( + "shares in venue DAI: ", + mgov.venueTotalShares(address(pcvDepositDai)) ); - } - - function testWithdrawWithLosses() public { - uint120 vconAmount = 1000e18; - testSystemThreeUsersLastNoDeposit(vconAmount); - - pcvDepositUsdc.setLastRecordedProfit(0); /// all profits are lost usdc venue - pcvDepositDai.setLastRecordedProfit(0); /// all profits are lost dai venue - mgov.accrueVcon(address(pcvDepositUsdc)); - - uint256 startingVconBalance = mgov.venueUserDepositedVcon( + mgov.unstake( + shareAmount, address(pcvDepositUsdc), + address(pcvDepositDai), + address(pcvSwapper), address(this) ); - int256 pendingRewards = mgov.getPendingRewards( - address(this), - address(pcvDepositUsdc) - ); - address[] memory venues = new address[](1); - venues[0] = address(pcvDepositUsdc); - mgov.realizeGainsAndLosses(venues); - - uint256 endingVconBalance = mgov.venueUserDepositedVcon( - address(pcvDepositUsdc), - address(this) - ); + uint256 endingVconBalance = vcon.balanceOf(address(this)); - assertEq( - pendingRewards, - endingVconBalance.toInt256() - startingVconBalance.toInt256() - ); - assertEq( - mgov - .getPendingRewards(address(this), address(pcvDepositUsdc)) - .toUint256(), - 0 - ); + assertTrue(endingVconBalance > startingVconBalance); } + // function testWithdrawWithLosses() public { + // uint120 vconAmount = 1000e18; + // testSystemThreeUsersLastNoDeposit(vconAmount); + + // pcvDepositUsdc.setLastRecordedProfit(0); /// all profits are lost usdc venue + // pcvDepositDai.setLastRecordedProfit(0); /// all profits are lost dai venue + + // mgov.accrueVcon(address(pcvDepositUsdc)); + + // uint256 startingVconBalance = mgov.venueUserShares( + // address(pcvDepositUsdc), + // address(this) + // ); + // int256 pendingRewards = mgov.getPendingRewards( + // address(this), + // address(pcvDepositUsdc) + // ); + + // address[] memory venues = new address[](1); + // venues[0] = address(pcvDepositUsdc); + // mgov.realizeGainsAndLosses(venues); + + // uint256 endingVconBalance = mgov.venueUserShares( + // address(pcvDepositUsdc), + // address(this) + // ); + + // assertEq( + // pendingRewards, + // endingVconBalance.toInt256() - startingVconBalance.toInt256() + // ); + // assertEq( + // mgov + // .getPendingRewards(address(this), address(pcvDepositUsdc)) + // .toUint256(), + // 0 + // ); + // } + function testSetProfitToVconRatio(address venue, uint256 ratio) public { uint256 oldProfitToVconRatio = mgov.profitToVconRatio(venue); From 047adbb4f2a0aa4c48c68164523d9592d34eccf5 Mon Sep 17 00:00:00 2001 From: Elliot Date: Mon, 30 Jan 2023 17:52:42 -0800 Subject: [PATCH 46/74] add additional unit tests --- test/unit/vcon/MarketGovernance.t.sol | 428 ++++++++++++++++++-------- 1 file changed, 299 insertions(+), 129 deletions(-) diff --git a/test/unit/vcon/MarketGovernance.t.sol b/test/unit/vcon/MarketGovernance.t.sol index 7d465438b..7e0d46c3d 100644 --- a/test/unit/vcon/MarketGovernance.t.sol +++ b/test/unit/vcon/MarketGovernance.t.sol @@ -1,6 +1,7 @@ pragma solidity 0.8.13; import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {console} from "@forge-std/console.sol"; import {MockERC20} from "@test/mock/MockERC20.sol"; @@ -11,6 +12,7 @@ import {SystemUnitTest} from "@test/unit/system/System.t.sol"; import {MockPCVDepositV3} from "@test/mock/MockPCVDepositV3.sol"; import {MarketGovernance} from "@voltprotocol/vcon/MarketGovernance.sol"; import {IMarketGovernance} from "@voltprotocol/vcon/IMarketGovernance.sol"; +import {ERC20HoldingPCVDeposit} from "@test/mock/ERC20HoldingPCVDeposit.sol"; import {TestAddresses as addresses} from "@test/unit/utils/TestAddresses.sol"; contract UnitTestMarketGovernance is SystemUnitTest { @@ -18,6 +20,8 @@ contract UnitTestMarketGovernance is SystemUnitTest { MarketGovernance public mgov; MockPCVSwapper public pcvSwapper; + ERC20HoldingPCVDeposit public daiHoldingDeposit; + ERC20HoldingPCVDeposit public usdcHoldingDeposit; uint256 public profitToVconRatioUsdc = 5e12; /// for each 1 wei in profit, 5e12 wei of vcon is received uint256 public profitToVconRatioDai = 5; /// for each 1 wei in profit, 5 wei of vcon is received @@ -44,6 +48,17 @@ contract UnitTestMarketGovernance is SystemUnitTest { function setUp() public override { super.setUp(); + daiHoldingDeposit = new ERC20HoldingPCVDeposit( + coreAddress, + IERC20(address(dai)), + address(0) + ); + usdcHoldingDeposit = new ERC20HoldingPCVDeposit( + coreAddress, + IERC20(address(usdc)), + address(0) + ); + /// can only swap from dai to usdc pcvSwapper = new MockPCVSwapper( MockERC20(address(dai)), @@ -57,14 +72,10 @@ contract UnitTestMarketGovernance is SystemUnitTest { vm.startPrank(addresses.governorAddress); - mgov.setProfitToVconRatio(address(pcvDepositDai), profitToVconRatioDai); - mgov.setProfitToVconRatio( - address(pcvDepositUsdc), - profitToVconRatioUsdc - ); - core.grantPCVController(address(mgov)); core.grantLocker(address(mgov)); + core.grantLocker(address(daiHoldingDeposit)); + core.grantLocker(address(usdcHoldingDeposit)); address[] memory swapper = new address[](1); swapper[0] = address(pcvSwapper); @@ -74,6 +85,31 @@ contract UnitTestMarketGovernance is SystemUnitTest { core.createRole(VoltRoles.PCV_MOVER, VoltRoles.GOVERNOR); core.grantRole(VoltRoles.PCV_MOVER, address(mgov)); + address[] memory venuesToAdd = new address[](2); + venuesToAdd[0] = address(daiHoldingDeposit); + venuesToAdd[1] = address(usdcHoldingDeposit); + + address[] memory oraclesToAdd = new address[](2); + oraclesToAdd[0] = address(daiConstantOracle); + oraclesToAdd[1] = address(usdcConstantOracle); + + pcvOracle.addVenues(venuesToAdd, oraclesToAdd); + + mgov.setProfitToVconRatio(address(pcvDepositDai), profitToVconRatioDai); + mgov.setProfitToVconRatio( + address(pcvDepositUsdc), + profitToVconRatioUsdc + ); + + mgov.setUnderlyingTokenHolderDeposit( + address(dai), + address(daiHoldingDeposit) + ); + mgov.setUnderlyingTokenHolderDeposit( + address(usdc), + address(usdcHoldingDeposit) + ); + vm.stopPrank(); } @@ -207,10 +243,18 @@ contract UnitTestMarketGovernance is SystemUnitTest { assertEq(endingTotalSupply, startingTotalSupply + vconAmount); assertTrue( - mgov.getVenueDeviation(address(pcvDepositUsdc), totalPCV) < 0 + mgov.getVenueDeviation( + address(pcvDepositUsdc), + totalPCV, + endingTotalSupply + ) < 0 ); /// underweight USDC balance assertTrue( - mgov.getVenueDeviation(address(pcvDepositDai), totalPCV) > 0 + mgov.getVenueDeviation( + address(pcvDepositDai), + totalPCV, + endingTotalSupply + ) > 0 ); /// overweight DAI balance } @@ -235,11 +279,19 @@ contract UnitTestMarketGovernance is SystemUnitTest { assertEq(vcon.balanceOf(user), 0); assertEq(endingTotalSupply, startingTotalSupply + vconAmount); assertTrue( - mgov.getVenueDeviation(address(pcvDepositUsdc), totalPCV) < 0 - ); /// underweight USDC balance + mgov.getVenueDeviation( + address(pcvDepositUsdc), + totalPCV, + endingTotalSupply + ) < 0 + ); /// underweight USDC balance in venues compared to staked vcon assertTrue( - mgov.getVenueDeviation(address(pcvDepositDai), totalPCV) > 0 - ); /// overweight DAI balance + mgov.getVenueDeviation( + address(pcvDepositDai), + totalPCV, + endingTotalSupply + ) > 0 + ); /// overweight DAI balance in venues compared to staked vcon } function testUserDepositsNoMove(uint120 vconAmount) public { @@ -262,10 +314,18 @@ contract UnitTestMarketGovernance is SystemUnitTest { assertEq(vcon.balanceOf(user), 0); assertEq(endingTotalSupply, startingTotalSupply + vconAmount); assertTrue( - mgov.getVenueDeviation(address(pcvDepositUsdc), totalPCV) < 0 + mgov.getVenueDeviation( + address(pcvDepositUsdc), + totalPCV, + endingTotalSupply + ) < 0 ); /// underweight USDC assertTrue( - mgov.getVenueDeviation(address(pcvDepositDai), totalPCV) > 0 + mgov.getVenueDeviation( + address(pcvDepositDai), + totalPCV, + endingTotalSupply + ) > 0 ); /// overweight DAI } @@ -282,8 +342,6 @@ contract UnitTestMarketGovernance is SystemUnitTest { mgov.unstake( startingShareAmount, address(pcvDepositUsdc), - address(pcvDepositDai), - address(pcvSwapper), address(this) ); @@ -299,27 +357,27 @@ contract UnitTestMarketGovernance is SystemUnitTest { testSystemOneUser(); assertEq(vcon.balanceOf(address(this)), 0); - uint256 vconAmount = mgov.getTotalVconStaked(); - - mgov.unstake( - vconAmount, + uint256 shareAmount = mgov.venueUserShares( address(pcvDepositUsdc), - address(pcvDepositDai), - address(pcvSwapper), address(this) ); + uint256 vconAmount = mgov.sharesToVcon( + address(pcvDepositUsdc), + shareAmount + ); + + mgov.unstake(shareAmount, address(pcvDepositUsdc), address(this)); assertEq(vcon.balanceOf(address(this)), vconAmount); - /// all funds moved to DAI PCV deposit + /// all funds moved to DAI Holding PCV deposit assertEq( pcvOracle.getTotalPcv(), - pcvOracle.getVenueBalance(address(pcvDepositDai)) + pcvOracle.getVenueBalance(address(usdcHoldingDeposit)) ); assertEq(0, mgov.venueTotalShares(address(pcvDepositDai))); - - assertEq(0, pcvOracle.getVenueBalance(address(pcvDepositUsdc))); assertEq(0, mgov.venueTotalShares(address(pcvDepositUsdc))); + assertEq(0, pcvOracle.getVenueBalance(address(pcvDepositUsdc))); assertEq(0, mgov.getTotalVconStaked()); } @@ -329,61 +387,132 @@ contract UnitTestMarketGovernance is SystemUnitTest { assertEq(vcon.balanceOf(address(this)), 0); - uint256 startingVconStakedAmount = mgov.getTotalVconStaked(); uint256 vconAmount = mgov.venueUserShares( address(pcvDepositUsdc), address(this) ); - mgov.unstake( - vconAmount, - address(pcvDepositUsdc), - address(pcvDepositDai), - address(pcvSwapper), - address(this) - ); + mgov.unstake(vconAmount, address(pcvDepositUsdc), address(this)); assertEq(vcon.balanceOf(address(this)), vconAmount); - /// all funds moved to DAI PCV deposit + { + assertEq(0, vcon.balanceOf(userOne)); + + uint256 shareAmount = mgov.venueUserShares( + address(pcvDepositDai), + userOne + ); + vm.startPrank(userOne); + mgov.unstake(shareAmount, address(pcvDepositDai), userOne); + + assertEq( + mgov.sharesToVcon(address(pcvDepositDai), shareAmount), + vcon.balanceOf(userOne) + ); + } + + /// half of funds moved to DAI PCV deposit assertEq( - pcvOracle.getTotalPcv(), - pcvOracle.getVenueBalance(address(pcvDepositDai)) + pcvOracle.getTotalPcv() / 2, + pcvOracle.getVenueBalance(address(usdcHoldingDeposit)) ); + + /// half of funds moved to DAI PCV deposit assertEq( - startingVconStakedAmount - vconAmount, - mgov.getTotalVconStaked() + pcvOracle.getTotalPcv() / 2, + pcvOracle.getVenueBalance(address(daiHoldingDeposit)) ); + assertEq(0, mgov.getTotalVconStaked()); assertEq(0, pcvOracle.getVenueBalance(address(pcvDepositUsdc))); assertEq(0, mgov.venueTotalShares(address(pcvDepositUsdc))); } - /// test withdrawing when src and dest are equal - function testWithdrawFailsSrcDestEqual() public { - uint256 vconAmount = mgov.getTotalVconStaked(); + struct DepositInfo { + address user; + uint120 vconAmount; + uint8 venue; + } - vm.expectRevert("MarketGovernance: src and dest equal"); - mgov.unstake( - vconAmount, - address(pcvDepositUsdc), - address(pcvDepositUsdc), - address(0), - address(this) + function testMultipleUsersStake(DepositInfo[] memory users) public { + unchecked { + for (uint256 i = 0; i < users.length; i++) { + address user = users[i].user; + if (user == address(0)) { + /// do not allow 0 address user + users[i].user = address( + uint160(block.timestamp + block.number + i) + ); + } + } + } + + uint256 totalPcv = pcvOracle.getTotalPcv(); + uint256 totalVconStaked = 0; + uint256 daiVconStaked = 0; + uint256 usdcVconStaked = 0; + + unchecked { + for (uint256 i = 0; i < users.length; i++) { + address user = users[i].user; + uint256 amount = users[i].vconAmount; + totalVconStaked += amount; + + vcon.mint(user, amount); + + vm.startPrank(user); + vcon.approve(address(mgov), amount); + + if (users[i].venue % 2 == 0) { + daiVconStaked += amount; + mgov.stake(amount, address(pcvDepositDai)); + } else { + usdcVconStaked += amount; + mgov.stake(amount, address(pcvDepositUsdc)); + } + } + } + + int256 daiAmount = mgov.getVenueDeviation( + address(pcvDepositDai), + totalPcv, + totalVconStaked ); + + IMarketGovernance.Rebalance[] + memory balance = new IMarketGovernance.Rebalance[](1); + + if (daiAmount > 0) { + /// over allocated DAI + balance[0] = IMarketGovernance.Rebalance({ + source: address(pcvDepositDai), + destination: address(pcvDepositUsdc), + swapper: address(pcvSwapper), + amountPcv: daiAmount.toUint256() + }); + } else { + /// under allocated DAI + balance[0] = IMarketGovernance.Rebalance({ + source: address(pcvDepositUsdc), + destination: address(pcvDepositDai), + swapper: address(pcvSwapper), + amountPcv: daiAmount.toUint256() / 1e12 + }); + } + + mgov.rebalance(balance); + + // assertEq(mgov.getVenueDeviation(address(pcvDepositDai), totalPcv, totalVconStaked), 0); + // assertEq(mgov.getVenueDeviation(address(pcvDepositUsdc), totalPcv, totalVconStaked), 0); } + /// Gain and loss scenarios + function testWithdrawWithGains() public { uint120 vconAmount = 1000e18; testSystemThreeUsersLastNoDeposit(vconAmount); - IMarketGovernance.PCVDepositInfo[] memory info = mgov - .getExpectedPCVAmounts(); - for (uint256 i = 0; i < info.length; i++) { - console.log("deposit: ", info[i].deposit); - console.log("amount: ", info[i].amount); - } - pcvDepositUsdc.setLastRecordedProfit(20_000e6); pcvDepositDai.setLastRecordedProfit(20_000e18); @@ -400,68 +529,45 @@ contract UnitTestMarketGovernance is SystemUnitTest { address(this) ); - console.log( - "shares in venue USDC: ", - mgov.venueTotalShares(address(pcvDepositUsdc)) - ); - console.log( - "shares in venue DAI: ", - mgov.venueTotalShares(address(pcvDepositDai)) - ); + mgov.unstake(shareAmount, address(pcvDepositUsdc), address(this)); - mgov.unstake( - shareAmount, - address(pcvDepositUsdc), + uint256 endingVconBalance = vcon.balanceOf(address(this)); + + assertTrue(endingVconBalance > startingVconBalance); + } + + function testWithdrawWithLossesFailsDai() public { + uint120 vconAmount = 1000e18; + testSystemThreeUsersLastNoDeposit(vconAmount); + pcvDepositDai.setLastRecordedProfit(0); + + uint256 shareAmount = mgov.venueUserShares( address(pcvDepositDai), - address(pcvSwapper), address(this) ); - uint256 endingVconBalance = vcon.balanceOf(address(this)); + vm.expectRevert("MarketGovernance: loss scenario"); + mgov.unstake(shareAmount, address(pcvDepositDai), address(this)); + } - assertTrue(endingVconBalance > startingVconBalance); + function testWithdrawWithLossesFailsUsdc() public { + uint120 vconAmount = 1000e18; + testSystemThreeUsersLastNoDeposit(vconAmount); + pcvDepositUsdc.setLastRecordedProfit(0); + + uint256 shareAmount = mgov.venueUserShares( + address(pcvDepositUsdc), + address(this) + ); + + vm.expectRevert("MarketGovernance: loss scenario"); + mgov.unstake(shareAmount, address(pcvDepositUsdc), address(this)); } - // function testWithdrawWithLosses() public { - // uint120 vconAmount = 1000e18; - // testSystemThreeUsersLastNoDeposit(vconAmount); - - // pcvDepositUsdc.setLastRecordedProfit(0); /// all profits are lost usdc venue - // pcvDepositDai.setLastRecordedProfit(0); /// all profits are lost dai venue - - // mgov.accrueVcon(address(pcvDepositUsdc)); - - // uint256 startingVconBalance = mgov.venueUserShares( - // address(pcvDepositUsdc), - // address(this) - // ); - // int256 pendingRewards = mgov.getPendingRewards( - // address(this), - // address(pcvDepositUsdc) - // ); - - // address[] memory venues = new address[](1); - // venues[0] = address(pcvDepositUsdc); - // mgov.realizeGainsAndLosses(venues); - - // uint256 endingVconBalance = mgov.venueUserShares( - // address(pcvDepositUsdc), - // address(this) - // ); - - // assertEq( - // pendingRewards, - // endingVconBalance.toInt256() - startingVconBalance.toInt256() - // ); - // assertEq( - // mgov - // .getPendingRewards(address(this), address(pcvDepositUsdc)) - // .toUint256(), - // 0 - // ); - // } - - function testSetProfitToVconRatio(address venue, uint256 ratio) public { + function testSetProfitToVconRatio(uint8 venueNumber, uint256 ratio) public { + address venue = venueNumber % 2 == 0 + ? address(daiHoldingDeposit) + : address(usdcHoldingDeposit); uint256 oldProfitToVconRatio = mgov.profitToVconRatio(venue); vm.expectEmit(true, true, false, true, address(mgov)); @@ -487,7 +593,7 @@ contract UnitTestMarketGovernance is SystemUnitTest { /// todo test withdrawing /// todo test withdrawing when there are profits - /// todo test withdrawing when there are losses + /// todo test withdraw failing when there are losses /// todo test stake, unstake, rebalance, accrueVcon, realize gains /// and losses with invalid venues to ensure reverts @@ -501,33 +607,43 @@ contract UnitTestMarketGovernance is SystemUnitTest { mgov.accrueVcon(address(0)); } + function testAccrueInLossVenueFails() public { + pcvDepositDai.setLastRecordedProfit(20_000e18); + mgov.accrueVcon(address(pcvDepositDai)); + + pcvDepositDai.setLastRecordedProfit(0); + + vm.expectRevert("MarketGovernance: loss scenario"); + mgov.accrueVcon(address(pcvDepositDai)); + } + + function testSetProfitToVconRatioFailsInvalidVenue() public { + vm.expectRevert("MarketGovernance: invalid venue"); + vm.prank(addresses.governorAddress); + mgov.setProfitToVconRatio(address(0), 0); + } + function testUnstakeInvalidSourceVenueFails() public { vm.expectRevert("MarketGovernance: invalid source"); - mgov.unstake(0, address(0), address(0), address(0), address(0)); + mgov.unstake(0, address(0), address(0)); } - function testUnstakeInvalidDestinationVenueFails() public { - vm.expectRevert("MarketGovernance: invalid destination"); - mgov.unstake( - 0, - address(pcvDepositDai), - address(0), - address(0), - address(0) + function testSetUnderlyingDepositFailsUnderlyingMismatch() public { + vm.expectRevert("MarketGovernance: underlying mismatch"); + vm.prank(addresses.governorAddress); + mgov.setUnderlyingTokenHolderDeposit( + address(usdc), + address(daiHoldingDeposit) ); } - function testUnstakeSrcAndDestVenueEqualFails() public { - vm.expectRevert("MarketGovernance: src and dest equal"); - mgov.unstake( - 0, - address(pcvDepositDai), - address(pcvDepositDai), - address(0), - address(0) - ); + function testSetUnderlyingDepositFailsInvalidVenue() public { + vm.expectRevert("MarketGovernance: invalid venue"); + vm.prank(addresses.governorAddress); + mgov.setUnderlyingTokenHolderDeposit(address(usdc), address(0)); } + /// ACL tests function testSetProfitToVconRatioFailsNonGovernor() public { vm.expectRevert("CoreRef: Caller is not a governor"); mgov.setProfitToVconRatio(address(0), 0); @@ -537,4 +653,58 @@ contract UnitTestMarketGovernance is SystemUnitTest { vm.expectRevert("CoreRef: Caller is not a governor"); mgov.setPCVRouter(address(0)); } + + function testSetUnderlyingDepositFailsNonGovernor() public { + vm.expectRevert("CoreRef: Caller is not a governor"); + mgov.setUnderlyingTokenHolderDeposit( + address(dai), + address(daiHoldingDeposit) + ); + } + + //// Pause tests + + function testPause() public { + vm.prank(addresses.governorAddress); + mgov.pause(); + + assertTrue(mgov.paused()); + } + + function testRebalanceFailsWhenPaused() public { + testPause(); + + IMarketGovernance.Rebalance[] + memory balance = new IMarketGovernance.Rebalance[](1); + balance[0] = IMarketGovernance.Rebalance({ + source: address(pcvDepositDai), + destination: address(pcvDepositUsdc), + swapper: address(pcvSwapper), + amountPcv: pcvDepositDai.balance() + }); + vm.expectRevert("Pausable: paused"); + + mgov.rebalance(balance); + } + + function testStakingFailsWhenPaused() public { + testPause(); + + vm.expectRevert("Pausable: paused"); + mgov.stake(0, address(0)); + } + + function testUnstakingFailsWhenPaused() public { + testPause(); + + vm.expectRevert("Pausable: paused"); + mgov.unstake(0, address(0), address(this)); + } + + function testAccrueFailsWhenPaused() public { + testPause(); + + vm.expectRevert("Pausable: paused"); + mgov.accrueVcon(address(0)); + } } From 32192facfa3affd276c5c54100b054352cfc4fd4 Mon Sep 17 00:00:00 2001 From: Elliot Date: Mon, 30 Jan 2023 17:53:32 -0800 Subject: [PATCH 47/74] add last recorded profit to ERC20 Holding PCV Deposit --- test/mock/ERC20HoldingPCVDeposit.sol | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/mock/ERC20HoldingPCVDeposit.sol b/test/mock/ERC20HoldingPCVDeposit.sol index df31d56a1..3c03c83bd 100644 --- a/test/mock/ERC20HoldingPCVDeposit.sol +++ b/test/mock/ERC20HoldingPCVDeposit.sol @@ -22,6 +22,8 @@ contract ERC20HoldingPCVDeposit is PCVDeposit, IERC20HoldingPCVDeposit { /// @notice Token which the balance is reported in IERC20 public immutable override token; + uint128 public lastRecordedProfit; + /// @notice WETH contract IWETH public immutable weth; @@ -40,6 +42,11 @@ contract ERC20HoldingPCVDeposit is PCVDeposit, IERC20HoldingPCVDeposit { return token.balanceOf(address(this)); } + /// @notice no-op on the holding deposit + function accrue() public globalLock(2) returns (uint256) { + return balance(); + } + /// @notice display the related token of the balance reported function balanceReportedIn() public view override returns (address) { return address(token); From 4d4cec0369ad7daabcc987412190741d367aa048 Mon Sep 17 00:00:00 2001 From: Elliot Date: Mon, 30 Jan 2023 17:55:22 -0800 Subject: [PATCH 48/74] Market Governance share price refactor, add holding deposit logic --- src/vcon/IMarketGovernance.sol | 10 +- src/vcon/MarketGovernance.sol | 270 ++++++++++++++++++--------------- 2 files changed, 154 insertions(+), 126 deletions(-) diff --git a/src/vcon/IMarketGovernance.sol b/src/vcon/IMarketGovernance.sol index 55236a074..8f7be808b 100644 --- a/src/vcon/IMarketGovernance.sol +++ b/src/vcon/IMarketGovernance.sol @@ -85,6 +85,12 @@ interface IMarketGovernance { uint256 profitIndex ); + /// @notice + event UnderlyingTokenDepositUpdated( + address indexed token, + address indexed venue + ); + /// ---------- Permissionless User PCV Allocation Methods ---------- /// stake VCON on a venue @@ -97,14 +103,10 @@ interface IMarketGovernance { /// based on the user's total amount of staked VCON /// @param amountVcon to stake /// @param source pcv deposit to pull funds from - /// @param destination pcv deposit to send funds - /// @param swapper address to swap tokens /// @param vconRecipient address to receive VCON tokens function unstake( uint256 amountVcon, address source, - address destination, - address swapper, address vconRecipient ) external; diff --git a/src/vcon/MarketGovernance.sol b/src/vcon/MarketGovernance.sol index e72aa2dce..c1490d87c 100644 --- a/src/vcon/MarketGovernance.sol +++ b/src/vcon/MarketGovernance.sol @@ -45,15 +45,9 @@ import {console} from "@forge-std/console.sol"; /// /// Anytime the rewards share price changes, so does the unclaimed user VCON rewards. /// -/// Issues I'm seeing so far -/// 1. if a loss occurs in a venue, then everyone's VCON balance gets marked down. -/// However, mark downs only occur when a loss is realized. This means unstaking after you have first realized -/// a loss and someone else hasn't, your pro-rata portion will be less, because the amount of VCON you have staked -/// has gone down. -/// 2. if a gain occurs, everyone's VCON balance marks up. -/// However, mark ups only occur when a gain is realized. This means unstaking after you have realized a gain and -/// someone else hasn't, your pro-rata portion of the PCV will be more than it would be if everyone had realized -/// their gains. +/// @dev users can stake on PCV deposits that are not productive, such as the holding deposit, +/// however, they will receive no rewards for doing so if the profit to VCON ratio is not set. +/// The PSM will not be able to be staked on as it is not whitelisted in the PCV Oracle. contract MarketGovernance is CoreRefV2, IMarketGovernance { using DeviationWeiGranularity for *; using SafeERC20 for *; @@ -82,6 +76,11 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { /// @notice total vcon deposited per venue mapping(address => uint256) public venueTotalShares; + /// @notice map an underlying token to the corresponding holding deposit + mapping(address => address) public underlyingTokenToHoldingDeposit; + + /// no balance checks when unstaking + /// ---------- Per Venue User Profit Tracking ---------- /// @dev convention for all double nested address mappings is key (venue -> user) -> value @@ -100,7 +99,7 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { /// @notice update the VCON share price and the last recorded profit for a given venue /// @param venue address to accrue - function accrueVcon(address venue) external globalLock(1) { + function accrueVcon(address venue) external globalLock(1) whenNotPaused { IPCVOracle oracle = pcvOracle(); require(oracle.isVenue(venue), "MarketGovernance: invalid destination"); @@ -117,7 +116,7 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { function stake( uint256 amountVcon, address destination - ) external globalLock(1) { + ) external globalLock(1) whenNotPaused { IPCVOracle oracle = pcvOracle(); require( oracle.isVenue(destination), @@ -145,27 +144,30 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { /// @notice unstake VCON and transfer corresponding VCON to another venue /// @param shareAmount the amount of shares to unstake /// @param source address to accrue rewards to, and pull funds from - /// @param destination address to send funds - /// @param swapper address to swap funds through /// @param vconRecipient address to receive the VCON + /// @dev both source and destination are checked twice, + /// the first time in the market governance contract, + /// the second time in the PCV Router contract. function unstake( uint256 shareAmount, address source, - address destination, - address swapper, address vconRecipient - ) external globalLock(1) { + ) external globalLock(1) whenNotPaused { /// ---------- Checks ---------- - IPCVOracle oracle = pcvOracle(); require(oracle.isVenue(source), "MarketGovernance: invalid source"); require( - oracle.isVenue(destination), - "MarketGovernance: invalid destination" + venueUserShares[source][msg.sender] >= shareAmount, + "MarketGovernance: invalid share amount" ); - require(source != destination, "MarketGovernance: src and dest equal"); + address denomination = IPCVDeposit(source).balanceReportedIn(); + address destination = underlyingTokenToHoldingDeposit[denomination]; + require( + destination != address(0), + "MarketGovernance: invalid destination" + ); /// ---------- Effects ---------- _accrue(source); /// update profitPerVCON in the source so the user gets paid out at the current share price @@ -174,38 +176,6 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { /// amount of PCV to withdraw is the amount vcon * venue balance / total vcon staked on venue uint256 amountPcv = getProRataPCVAmounts(source, shareAmount); - uint256 amountVcon = sharesToVcon(source, shareAmount); - - /// read unsafe because we are at lock level 1 - uint256 totalPcv = pcvOracle().getTotalPcvUnsafe(); - - /// record how balanced the system is before the PCV movement - uint256 totalVconStaked = getTotalVconStaked(); - - uint256 sourceExpectedPcv = getExpectedVenuePCVAmount( - source, - totalPcv, - totalVconStaked - ); - uint256 destinationExpectedPcv = getExpectedVenuePCVAmount( - source, - totalPcv, - totalVconStaked - ); - - int256 sourceVenueBalance = getVenueDeviation( - source, - sourceExpectedPcv - ); - int256 destinationVenueBalance = getVenueDeviation( - destination, - destinationExpectedPcv - ); - - require( - venueUserShares[source][msg.sender] >= shareAmount, - "MarketGovernance: invalid share amount" - ); /// user updates venueUserShares[source][msg.sender] -= shareAmount; @@ -215,19 +185,22 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { /// ---------- Interactions ---------- - vcon().safeTransfer(vconRecipient, amountVcon); /// transfer VCON amount to recipient - - _movePCVWithChecks( - source, - destination, - swapper, - amountPcv, - totalPcv, - sourceVenueBalance, - destinationVenueBalance - ); + { + PCVRouter(pcvRouter).movePCV( + source, + destination, + address(0), + amountPcv, + denomination, + denomination + ); + } - emit Unstaked(source, msg.sender, amountVcon, amountPcv); + { + uint256 amountVcon = sharesToVcon(source, shareAmount); + vcon().safeTransfer(vconRecipient, amountVcon); /// transfer VCON amount to recipient + emit Unstaked(source, msg.sender, amountVcon, amountPcv); + } } /// @notice rebalance PCV without staking or unstaking VCON @@ -235,9 +208,12 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { /// as a whole, otherwise it will revert /// @param movements information on all pcv movements /// including sources, destinations, amounts and swappers - function rebalance(Rebalance[] calldata movements) external globalLock(1) { + function rebalance( + Rebalance[] calldata movements + ) external globalLock(1) whenNotPaused { /// read unsafe because we are at lock level 1 uint256 totalPcv = pcvOracle().getTotalPcvUnsafe(); + uint256 totalVconStaked = getTotalVconStaked(); unchecked { for (uint256 i = 0; i < movements.length; i++) { @@ -245,11 +221,17 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { address destination = movements[i].destination; address swapper = movements[i].swapper; uint256 amountPcv = movements[i].amountPcv; + /// record how balanced the system is before the PCV movement - int256 sourceVenueBalance = getVenueDeviation(source, totalPcv); + int256 sourceVenueBalance = getVenueDeviation( + source, + totalPcv, + totalVconStaked + ); int256 destinationVenueBalance = getVenueDeviation( destination, - totalPcv + totalPcv, + totalVconStaked ); _movePCVWithChecks( @@ -259,7 +241,8 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { amountPcv, totalPcv, sourceVenueBalance, - destinationVenueBalance + destinationVenueBalance, + totalVconStaked ); } } @@ -318,38 +301,46 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { } } - /// @notice returns positive value if under allocated - /// returns negative value if over allocated + /// @notice returns positive value if over allocated + /// returns negative value if under allocated /// if no venue balance and deposited vcon, return positive /// if venue balance and no deposited vcon, return negative /// /// @param venue to query - /// @param expectedVenueBalance expected venue pcv + /// @param totalPcv expected venue pcv + /// @param totalVconStaked expected venue pcv function getVenueDeviation( address venue, - uint256 expectedVenueBalance + uint256 totalPcv, + uint256 totalVconStaked ) public view returns (int256) { uint256 venueBalance = pcvOracle().getVenueBalance(venue); + uint256 expectedVenueBalance = getExpectedVenuePCVAmount( + venue, + totalPcv, + totalVconStaked + ); return venueBalance.toInt256() - expectedVenueBalance.toInt256(); } /// @param venue to figure out total pro rata pcv - /// @param amountVcon to find total amount of pro rata pcv - /// @return the pro rata pcv controlled in the given venue based on the amount of VCON + /// @param shareAmount to find total amount of pro rata pcv + /// @return the pro rata pcv controlled in the given venue based on the amount of shares + /// returned amount will be used to call the PCV router, so return a non-decimal normalized value function getProRataPCVAmounts( address venue, - uint256 amountVcon + uint256 shareAmount ) public view returns (uint256) { uint256 venuePcv = IPCVDeposit(venue).balance(); - uint256 cachedVconStaked = sharesToVcon(venue, venueTotalShares[venue]); + uint256 cachedTotalShares = venueTotalShares[venue]; /// save a single warm SLOAD /// 0 checks as any 0 denominator will cause a revert - if (cachedVconStaked == 0) { + if (cachedTotalShares == 0) { return 0; /// perfectly balanced at 0 PCV or 0 VCON staked } - /// @audit we do not add 1 to the pro rata PCV here. This means a withdrawal of 1 Wei of VCON + /// @audit we do not add 1 to the pro rata PCV here. This means a withdrawal of 1 Wei of shares /// will allow removing a user's VCON without having to withdraw PCV from a venue. /// This is a known issue, however it is not harmful as it would require a quintillion withdrawals /// to withdraw 1 VCON, which would cost at minimum 5,000e18 gas per withdraw, meaning it would cost at least 1 million ether @@ -358,7 +349,7 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { /// of the losses. However, in a loss scenario, the unstake function does not allow execution if the user has an unrealized /// loss in that venue. This condition stops the aforementioned exploit. /// fix would require rounding up in the protocol's favor, so that a withdrawal of 1 Wei of VCON has an actual withdraw amount - uint256 proRataPcv = (amountVcon * venuePcv) / cachedVconStaked; + uint256 proRataPcv = (shareAmount * venuePcv) / cachedTotalShares; return proRataPcv; } @@ -419,7 +410,8 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { uint256 amountPcv, uint256 totalPcv, int256 sourceVenueBalance, - int256 destinationVenueBalance + int256 destinationVenueBalance, + uint256 totalVconStaked ) private { address sourceAsset = IPCVDepositV2(source).balanceReportedIn(); address destinationAsset = IPCVDepositV2(destination) @@ -436,28 +428,34 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { destinationAsset ); - /// TODO simplify this by passing delta of starting balance - /// instead of calling balance again on each pcv deposit - /// record how balanced the system is before the PCV movement - int256 sourceVenueBalanceAfter = getVenueDeviation(source, totalPcv); + if (sharesToVcon(source, venueTotalShares[source]) > 0) { + /// TODO simplify this by passing delta of starting balance + /// instead of calling balance again on each pcv deposit + /// record how balanced the system is before the PCV movement + + int256 sourceVenueBalanceAfter = getVenueDeviation( + source, + totalPcv, + totalVconStaked + ); + + /// source and dest venue balance measures the distance from being perfectly balanced + + /// validate source venue balance became more balanced + _checkBalance( + sourceVenueBalance, + sourceVenueBalanceAfter, + "MarketGovernance: src more imbalanced" + ); + } + int256 destinationVenueBalanceAfter = getVenueDeviation( destination, - totalPcv - ); - - /// source and dest venue balance measures the distance from being perfectly balanced - - console.logInt(sourceVenueBalance); - console.logInt(sourceVenueBalanceAfter); - - /// validate source venue balance became more balanced - _checkBalance( - sourceVenueBalance, - sourceVenueBalanceAfter, - "MarketGovernance: src more imbalanced" + totalPcv, + totalVconStaked ); - /// validate destination venue balance became more balanced + /// validate destination venue balance became more balanced if VCON is staked on it _checkBalance( destinationVenueBalance, destinationVenueBalanceAfter, @@ -466,6 +464,7 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { } /// @notice helper function to validate balance moved in the right direction after a pcv movement + /// if balance before and after are the same, return true function _checkBalance( int256 balanceBefore, int256 balanceAfter, @@ -479,8 +478,13 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { ); } - /// update the venue last recorded profit + /// @notice update the venue last recorded profit /// and the venue last recorded vcon share price + /// system must be at lock level 1 to call this function, + /// otherwise call to `accrue()` on the PCV Deposit will fail + /// @dev this function assumes the venue is in the PCV Oracle as the + /// calling function should either check, or be callable only by + /// governance function _accrue(address venue) private { /// cache starting recorded profit before the external call even though /// there is no way to call _accrue without setting the global reentrancy lock to level 1 @@ -489,46 +493,40 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { IPCVDepositV2(venue).accrue(); uint256 endingLastRecordedProfit = IPCVDepositV2(venue) - .lastRecordedProfit(); + .lastRecordedProfit(); /// think through calling the pcv oracle here uint256 endingLastRecordedSharePrice = venueLastRecordedVconSharePrice[ venue ]; + int256 venueProfit = (endingLastRecordedProfit.toInt256() - + startingLastRecordedProfit.toInt256()); + + require(venueProfit >= 0, "MarketGovernance: loss scenario"); /// update venue last recorded profit regardless /// of participation in market governance if (endingLastRecordedSharePrice != 0) { uint256 venueShares = venueTotalShares[venue]; - uint256 venueProfitRatio = profitToVconRatio[venue]; /// if venue has 0 staked vcon, do not update share price, just update profit index if (venueShares != 0) { - int256 venueProfit = (endingLastRecordedProfit.toInt256() - - startingLastRecordedProfit.toInt256()); - - int256 vconEarnedPerShare = (Constants.ETH_GRANULARITY_INT * - venueProfit * - venueProfitRatio.toInt256()) / venueShares.toInt256(); - - if (vconEarnedPerShare >= 0) { - /// gain scenario - venueLastRecordedVconSharePrice[venue] += ( - vconEarnedPerShare.toUint256() - ).toUint128(); - } else { - /// loss scenario - /// turn losses positive and subtract them - venueLastRecordedVconSharePrice[venue] -= ( - -vconEarnedPerShare - ).toUint256().toUint128(); - } + uint256 venueProfitRatio = profitToVconRatio[venue]; + + uint256 vconEarnedPerShare = (Constants.ETH_GRANULARITY * + venueProfit.toUint256() * + venueProfitRatio) / venueShares; + + /// gain scenario + venueLastRecordedVconSharePrice[venue] += vconEarnedPerShare + .toUint128(); } } else { - /// share price is 0, meaning it is not initialized, so initialize + /// share price is 0, and requires initialization venueLastRecordedVconSharePrice[venue] = Constants .ETH_GRANULARITY .toUint128(); } + /// update the venue's profit index venueLastRecordedProfit[venue] = endingLastRecordedProfit.toUint128(); emit VenueIndexUpdated( @@ -540,10 +538,17 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { /// ---------- Governor-Only Permissioned API ---------- + /// @notice governor only function to set the profit to VCON ratio + /// this function will not be callable if an underlying venue took a loss as `_accrue()` + /// will revert and no users will be able to withdraw their VCON. function setProfitToVconRatio( address venue, uint256 newProfitToVconRatio - ) external onlyGovernor { + ) external onlyGovernor globalLock(1) { + /// lock to level 1 so that accrue can succeed + require(pcvOracle().isVenue(venue), "MarketGovernance: invalid venue"); + _accrue(venue); /// ensure users receive all rewards from the old rate + uint256 oldProfitToVconRatio = profitToVconRatio[venue]; profitToVconRatio[venue] = newProfitToVconRatio; @@ -554,10 +559,31 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { ); } + /// @notice governor only function to set the PCV Router for market governance rebalances function setPCVRouter(address newPcvRouter) external onlyGovernor { address oldPcvRouter = pcvRouter; pcvRouter = newPcvRouter; emit PCVRouterUpdated(oldPcvRouter, newPcvRouter); } + + /// @notice governor only function to set a venue for market governance withdrawals in a given denomination + /// @param token underlying token for venue to handle + /// @param venue where funds of denomination `token` will be withdrawn to + /// @dev venue must be a whitelisted PCV Deposit, + /// and the venue's underlying token must match the token passed + function setUnderlyingTokenHolderDeposit( + address token, + address venue + ) external onlyGovernor { + require(pcvOracle().isVenue(venue), "MarketGovernance: invalid venue"); + require( + IPCVDeposit(venue).balanceReportedIn() == token, + "MarketGovernance: underlying mismatch" + ); + + underlyingTokenToHoldingDeposit[token] = venue; + + emit UnderlyingTokenDepositUpdated(token, venue); + } } From 9d1d6a6f314acde64ac172751417db4ec60e02d0 Mon Sep 17 00:00:00 2001 From: Elliot Date: Thu, 2 Feb 2023 02:21:59 -0800 Subject: [PATCH 49/74] Add more unit and fuzz tests --- test/unit/system/System.t.sol | 1 - test/unit/vcon/MarketGovernance.t.sol | 301 +++++++++++++++++++++++--- 2 files changed, 268 insertions(+), 34 deletions(-) diff --git a/test/unit/system/System.t.sol b/test/unit/system/System.t.sol index 9f9212345..afe54196b 100644 --- a/test/unit/system/System.t.sol +++ b/test/unit/system/System.t.sol @@ -297,7 +297,6 @@ contract SystemUnitTest is Test { vm.label(address(this), "address this"); vm.label(address(dai), "DAI"); vm.label(address(usdc), "USDC"); - // console.log("finished setting up system"); } function testSetup() public { diff --git a/test/unit/vcon/MarketGovernance.t.sol b/test/unit/vcon/MarketGovernance.t.sol index 7e0d46c3d..5d865dd0b 100644 --- a/test/unit/vcon/MarketGovernance.t.sol +++ b/test/unit/vcon/MarketGovernance.t.sol @@ -6,6 +6,7 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {console} from "@forge-std/console.sol"; import {MockERC20} from "@test/mock/MockERC20.sol"; import {VoltRoles} from "@voltprotocol/core/VoltRoles.sol"; +import {Constants} from "@voltprotocol/Constants.sol"; import {PCVRouter} from "@voltprotocol/pcv/PCVRouter.sol"; import {MockPCVSwapper} from "@test/mock/MockPCVSwapper.sol"; import {SystemUnitTest} from "@test/unit/system/System.t.sol"; @@ -101,11 +102,11 @@ contract UnitTestMarketGovernance is SystemUnitTest { profitToVconRatioUsdc ); - mgov.setUnderlyingTokenHolderDeposit( + mgov.setUnderlyingTokenHoldingDeposit( address(dai), address(daiHoldingDeposit) ); - mgov.setUnderlyingTokenHolderDeposit( + mgov.setUnderlyingTokenHoldingDeposit( address(usdc), address(usdcHoldingDeposit) ); @@ -429,13 +430,53 @@ contract UnitTestMarketGovernance is SystemUnitTest { assertEq(0, mgov.venueTotalShares(address(pcvDepositUsdc))); } + function _rebalance() private { + uint256 totalPcv = pcvOracle.getTotalPcv(); + uint256 totalVconStaked = mgov.getTotalVconStaked(); + + int256 daiAmount = mgov.getVenueDeviation( + address(pcvDepositDai), + totalPcv, + totalVconStaked + ); + + if (daiAmount == 0 || totalVconStaked == 0) { + return; + } + + IMarketGovernance.Rebalance[] + memory balance = new IMarketGovernance.Rebalance[](1); + + if (daiAmount > 0) { + /// over allocated DAI deposit + balance[0] = IMarketGovernance.Rebalance({ + source: address(pcvDepositDai), + destination: address(pcvDepositUsdc), + swapper: address(pcvSwapper), + amountPcv: (daiAmount).toUint256() - 1 + }); + mgov.rebalance(balance); + } else if ( + (-daiAmount).toUint256() / 1e12 <= pcvDepositUsdc.balance() + ) { + /// under allocated DAI deposit + balance[0] = IMarketGovernance.Rebalance({ + source: address(pcvDepositUsdc), + destination: address(pcvDepositDai), + swapper: address(pcvSwapper), + amountPcv: (-daiAmount).toUint256() / 1e12 + }); + mgov.rebalance(balance); + } + } + struct DepositInfo { address user; uint120 vconAmount; uint8 venue; } - function testMultipleUsersStake(DepositInfo[] memory users) public { + function testMultipleUsersStake(DepositInfo[15] memory users) public { unchecked { for (uint256 i = 0; i < users.length; i++) { address user = users[i].user; @@ -445,6 +486,10 @@ contract UnitTestMarketGovernance is SystemUnitTest { uint160(block.timestamp + block.number + i) ); } + if (users[i].vconAmount <= 1e18) { + /// no users will be depositing less than 1 VCON in the system + users[i].vconAmount += 1e18; + } } } @@ -471,44 +516,207 @@ contract UnitTestMarketGovernance is SystemUnitTest { usdcVconStaked += amount; mgov.stake(amount, address(pcvDepositUsdc)); } + + vm.stopPrank(); } } - int256 daiAmount = mgov.getVenueDeviation( - address(pcvDepositDai), - totalPcv, - totalVconStaked + _rebalance(); + + IMarketGovernance.PCVDepositInfo[] memory expectedOutput = mgov + .getExpectedPCVAmounts(); + + unchecked { + for (uint256 i = 0; i < expectedOutput.length; i++) { + if (expectedOutput[i].amount >= 1e18) { + assertApproxEq( + pcvOracle + .getVenueBalance(expectedOutput[i].deposit) + .toInt256(), + expectedOutput[i].amount.toInt256(), + 0 + ); + } + } + } + + assertEq(mgov.getTotalVconStaked(), totalVconStaked); + assertTrue( + mgov.getVenueDeviation( + address(pcvDepositDai), + totalPcv, + totalVconStaked + ) < 1e18 + ); + assertTrue( + mgov.getVenueDeviation( + address(pcvDepositUsdc), + totalPcv, + totalVconStaked + ) < 1e18 ); + } - IMarketGovernance.Rebalance[] - memory balance = new IMarketGovernance.Rebalance[](1); + function testMultipleUsersUnstake(DepositInfo[15] memory users) public { + testMultipleUsersStake(users); + uint256 totalVconStaked = 0; + uint256 totalPcv = pcvOracle.getTotalPcv(); - if (daiAmount > 0) { - /// over allocated DAI - balance[0] = IMarketGovernance.Rebalance({ - source: address(pcvDepositDai), - destination: address(pcvDepositUsdc), - swapper: address(pcvSwapper), - amountPcv: daiAmount.toUint256() - }); - } else { - /// under allocated DAI - balance[0] = IMarketGovernance.Rebalance({ - source: address(pcvDepositUsdc), - destination: address(pcvDepositDai), - swapper: address(pcvSwapper), - amountPcv: daiAmount.toUint256() / 1e12 - }); + unchecked { + for (uint256 i = 0; i < users.length; i++) { + address user = users[i].user; + uint256 vconAmount = users[i].vconAmount; + address venue = users[i].venue % 2 == 0 + ? address(pcvDepositDai) + : address(pcvDepositUsdc); + totalVconStaked += vconAmount; + + uint256 startingUserVconBalance = vcon.balanceOf(user); + uint256 shareAmount = mgov.vconToShares(venue, vconAmount); + + vm.prank(user); + mgov.unstake(shareAmount, venue, user); + + uint256 endingUserVconBalance = vcon.balanceOf(user); + + assertEq( + endingUserVconBalance - startingUserVconBalance, + vconAmount + ); + } } - mgov.rebalance(balance); + assertEq(mgov.getTotalVconStaked(), 0); + + /// by this point, all VCON should be unstaked so the amount of PCV in the venue should be minimal + assertTrue( + mgov.getVenueDeviation( + address(pcvDepositDai), + totalPcv, + totalVconStaked + ) < 1e18 + ); + assertTrue( + mgov.getVenueDeviation( + address(pcvDepositUsdc), + totalPcv, + totalVconStaked + ) < 1e6 + ); - // assertEq(mgov.getVenueDeviation(address(pcvDepositDai), totalPcv, totalVconStaked), 0); - // assertEq(mgov.getVenueDeviation(address(pcvDepositUsdc), totalPcv, totalVconStaked), 0); + /// assert 99.99999999999999999% of PCV withdrawn + assertTrue(pcvDepositDai.balance() < 2); + assertTrue(pcvDepositUsdc.balance() < 2); } - /// Gain and loss scenarios + function testStakeAndApplyLosses( + DepositInfo[15] memory users, + uint8 shareDenominator + ) public { + vm.assume(shareDenominator > 1); /// not 0 or 1 + testMultipleUsersStake(users); + + uint256 totalVconStaked = mgov.getTotalVconStaked(); + uint128 sharePrice = mgov.venueLastRecordedVconSharePrice( + address(pcvDepositDai) + ) / shareDenominator; + + /// mark things down + vm.startPrank(addresses.governorAddress); + mgov.applyVenueLosses(address(pcvDepositDai), sharePrice); + mgov.applyVenueLosses(address(pcvDepositUsdc), sharePrice); + vm.stopPrank(); + + assertApproxEq( + (totalVconStaked / shareDenominator).toInt256(), + mgov.getTotalVconStaked().toInt256(), + 0 + ); + } + + function testSharePriceStaysConstantNoSharesWithProfit() public { + uint128 startingSharePrice = mgov.venueLastRecordedVconSharePrice( + address(pcvDepositDai) + ); + uint128 startingLastRecordedProfit = mgov.venueLastRecordedProfit( + address(pcvDepositDai) + ); + + pcvDepositDai.setLastRecordedProfit(20_000e18); + mgov.accrueVcon(address(pcvDepositDai)); + + uint128 endingLastRecordedProfit = mgov.venueLastRecordedProfit( + address(pcvDepositDai) + ); + uint128 endingSharePrice = mgov.venueLastRecordedVconSharePrice( + address(pcvDepositDai) + ); + + assertEq(startingSharePrice, endingSharePrice); + assertTrue(endingLastRecordedProfit > startingLastRecordedProfit); + } + + /// apply gain x across period y with z amount of users + function testSharePriceStepWise( + uint96 periodGain, + uint8 periods, + address[15] memory users + ) public { + vm.assume(periodGain > 1e18); + uint256 vconAmount = 1e18; + for (uint256 i = 0; i < periods; ) { + address user = users[i % 15] == address(0) + ? address(uint160(i + 1)) + : users[i % 15]; + vcon.mint(user, vconAmount); + + vm.startPrank(user); + vcon.approve(address(mgov), vconAmount); + mgov.stake(vconAmount, address(pcvDepositDai)); + vm.stopPrank(); + + uint256 totalVconStaked = mgov.getVenueVconStaked( + address(pcvDepositDai) + ); + uint256 totalShares = mgov.venueTotalShares(address(pcvDepositDai)); + uint256 venueStartingPrice = mgov.venueLastRecordedVconSharePrice( + address(pcvDepositDai) + ); + uint256 vconRatio = mgov.profitToVconRatio(address(pcvDepositDai)); + + uint256 vconInflation = periodGain * vconRatio; + uint256 expectedVenueSharePrice = venueStartingPrice + + (Constants.ETH_GRANULARITY * vconInflation) / + totalShares; + + pcvDepositDai.setLastRecordedProfit( + pcvDepositDai.lastRecordedProfit() + periodGain + ); + + mgov.accrueVcon(address(pcvDepositDai)); + + assertApproxEq( + (expectedVenueSharePrice).toInt256(), + mgov + .venueLastRecordedVconSharePrice(address(pcvDepositDai)) + .toInt256(), + 0 + ); + assertApproxEq( + (vconInflation + totalVconStaked).toInt256(), + mgov.getVenueVconStaked(address(pcvDepositDai)).toInt256(), + 0 + ); + + unchecked { + i++; + } + /// apply gain + /// ensure getTotalVconStaked returns correct number + } + } + /// Gain and loss scenarios function testWithdrawWithGains() public { uint120 vconAmount = 1000e18; testSystemThreeUsersLastNoDeposit(vconAmount); @@ -533,7 +741,7 @@ contract UnitTestMarketGovernance is SystemUnitTest { uint256 endingVconBalance = vcon.balanceOf(address(this)); - assertTrue(endingVconBalance > startingVconBalance); + assertTrue(endingVconBalance > startingVconBalance); /// beef up these assertions to ensure share price is correct } function testWithdrawWithLossesFailsDai() public { @@ -631,7 +839,7 @@ contract UnitTestMarketGovernance is SystemUnitTest { function testSetUnderlyingDepositFailsUnderlyingMismatch() public { vm.expectRevert("MarketGovernance: underlying mismatch"); vm.prank(addresses.governorAddress); - mgov.setUnderlyingTokenHolderDeposit( + mgov.setUnderlyingTokenHoldingDeposit( address(usdc), address(daiHoldingDeposit) ); @@ -640,7 +848,29 @@ contract UnitTestMarketGovernance is SystemUnitTest { function testSetUnderlyingDepositFailsInvalidVenue() public { vm.expectRevert("MarketGovernance: invalid venue"); vm.prank(addresses.governorAddress); - mgov.setUnderlyingTokenHolderDeposit(address(usdc), address(0)); + mgov.setUnderlyingTokenHoldingDeposit(address(usdc), address(0)); + } + + function testApplyLossesFailsInvalidVenue() public { + vm.expectRevert("MarketGovernance: invalid venue"); + vm.prank(addresses.governorAddress); + mgov.applyVenueLosses(address(0), 1); + } + + function testApplyLossesFailsZeroSharePrice() public { + vm.expectRevert("MarketGovernance: cannot set share price to 0"); + vm.prank(addresses.governorAddress); + mgov.applyVenueLosses(address(pcvDepositDai), 0); + } + + function testApplyLossesFailsSharePriceMarkup() public { + mgov.accrueVcon(address(pcvDepositDai)); + uint128 sharePrice = mgov.venueLastRecordedVconSharePrice( + address(pcvDepositDai) + ); + vm.expectRevert("MarketGovernance: share price not less"); + vm.prank(addresses.governorAddress); + mgov.applyVenueLosses(address(pcvDepositDai), sharePrice + 10); } /// ACL tests @@ -656,12 +886,17 @@ contract UnitTestMarketGovernance is SystemUnitTest { function testSetUnderlyingDepositFailsNonGovernor() public { vm.expectRevert("CoreRef: Caller is not a governor"); - mgov.setUnderlyingTokenHolderDeposit( + mgov.setUnderlyingTokenHoldingDeposit( address(dai), address(daiHoldingDeposit) ); } + function testApplyLossesFailsNonGovernor() public { + vm.expectRevert("CoreRef: Caller is not a governor"); + mgov.applyVenueLosses(address(dai), 1); + } + //// Pause tests function testPause() public { From c9ed0a6cb71276330138221460f9117f571a4baa Mon Sep 17 00:00:00 2001 From: Elliot Date: Thu, 2 Feb 2023 02:23:28 -0800 Subject: [PATCH 50/74] interface and contract clean up --- src/vcon/IMarketGovernance.sol | 9 ++- src/vcon/MarketGovernance.sol | 118 +++++++++++++++++++++++++-------- 2 files changed, 99 insertions(+), 28 deletions(-) diff --git a/src/vcon/IMarketGovernance.sol b/src/vcon/IMarketGovernance.sol index 8f7be808b..d2b7a957e 100644 --- a/src/vcon/IMarketGovernance.sol +++ b/src/vcon/IMarketGovernance.sol @@ -85,12 +85,19 @@ interface IMarketGovernance { uint256 profitIndex ); - /// @notice + /// @notice emitted when the safe address for a given token denomination is updated event UnderlyingTokenDepositUpdated( address indexed token, address indexed venue ); + /// @notice emitted when share price is marked down through governance + event LossesApplied( + address indexed venue, + uint256 oldSharePrice, + uint256 newSharePrice + ); + /// ---------- Permissionless User PCV Allocation Methods ---------- /// stake VCON on a venue diff --git a/src/vcon/MarketGovernance.sol b/src/vcon/MarketGovernance.sol index c1490d87c..5cc90569c 100644 --- a/src/vcon/MarketGovernance.sol +++ b/src/vcon/MarketGovernance.sol @@ -15,6 +15,9 @@ import {DeviationWeiGranularity} from "@voltprotocol/utils/DeviationWeiGranulari import {console} from "@forge-std/console.sol"; +/// TODO cache pcv oracle total pcv so getAllPCV isn't needed to figure +/// out total pcv + /// @notice this contract requires the PCV Mover and Locker role /// /// If an account has an unrealized loss on a venue, they cannot do any other action on that venue @@ -41,7 +44,8 @@ import {console} from "@forge-std/console.sol"; /// Profit Per VCON = Profit Per VCON + ∆Cumulative Profits (Dollars) * VCON:Dollar Ratio / Venue VCON Staked /// /// Formula for calculating user VCON rewards: -/// User VCON rewards = (Profit Per VCON - User Starting Profit Per VCON) * VCON Staked +/// ∆Share Price = (Ending Share Price - Starting Share Price) +/// User VCON rewards = ∆Share Price * User Shares /// /// Anytime the rewards share price changes, so does the unclaimed user VCON rewards. /// @@ -94,9 +98,6 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { pcvRouter = _pcvRouter; } - /// TODO update pcv oracle to cache total pcv so getAllPCV isn't needed to figure - /// out if weights are correct - /// @notice update the VCON share price and the last recorded profit for a given venue /// @param venue address to accrue function accrueVcon(address venue) external globalLock(1) whenNotPaused { @@ -162,12 +163,13 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { "MarketGovernance: invalid share amount" ); - address denomination = IPCVDeposit(source).balanceReportedIn(); + address denomination = IPCVDepositV2(source).balanceReportedIn(); address destination = underlyingTokenToHoldingDeposit[denomination]; require( destination != address(0), "MarketGovernance: invalid destination" ); + /// ---------- Effects ---------- _accrue(source); /// update profitPerVCON in the source so the user gets paid out at the current share price @@ -212,9 +214,9 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { Rebalance[] calldata movements ) external globalLock(1) whenNotPaused { /// read unsafe because we are at lock level 1 - uint256 totalPcv = pcvOracle().getTotalPcvUnsafe(); + IPCVOracle oracle = pcvOracle(); + uint256 totalPcv = oracle.getTotalPcvUnsafe(); uint256 totalVconStaked = getTotalVconStaked(); - unchecked { for (uint256 i = 0; i < movements.length; i++) { address source = movements[i].source; @@ -222,6 +224,26 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { address swapper = movements[i].swapper; uint256 amountPcv = movements[i].amountPcv; + /// validate source, dest and swapper + require( + oracle.isVenue(source), + "MarketGovernance: invalid source" + ); + require( + oracle.isVenue(destination), + "MarketGovernance: invalid destination" + ); + /// if swapper is used, validate in PCV Router whiteliste + if (swapper != address(0)) { + require( + PCVRouter(pcvRouter).isPCVSwapper(swapper), + "MarketGovernance: invalid swapper" + ); + } + + /// call accrue on destination to ensure no unrealized losses have occured + _accrue(destination); + /// record how balanced the system is before the PCV movement int256 sourceVenueBalance = getVenueDeviation( source, @@ -283,6 +305,9 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { } /// get the total amount of VCON staked based on the last cached share prices of each venue + /// during a loss scenario, this function will return an incorrect total amount of VCON staked + /// because the share price in the venues have not been marked down, leading to an incorrect sum. + /// all functions that call this function during a loss scenario will also be incorrect function getTotalVconStaked() public view @@ -314,7 +339,7 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { uint256 totalPcv, uint256 totalVconStaked ) public view returns (int256) { - uint256 venueBalance = pcvOracle().getVenueBalance(venue); + uint256 venueBalance = pcvOracle().getVenueBalance(venue); /// decimal normalized balance uint256 expectedVenueBalance = getExpectedVenuePCVAmount( venue, totalPcv, @@ -332,12 +357,12 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { address venue, uint256 shareAmount ) public view returns (uint256) { - uint256 venuePcv = IPCVDeposit(venue).balance(); + uint256 venuePcv = IPCVDepositV2(venue).balance(); uint256 cachedTotalShares = venueTotalShares[venue]; /// save a single warm SLOAD /// 0 checks as any 0 denominator will cause a revert if (cachedTotalShares == 0) { - return 0; /// perfectly balanced at 0 PCV or 0 VCON staked + return 0; } /// @audit we do not add 1 to the pro rata PCV here. This means a withdrawal of 1 Wei of shares @@ -354,9 +379,21 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { return proRataPcv; } + /// @dev algorithm to find all rebalance actions necessary to get system to fully balanced + /// get expected PCV amounts in each venue + /// build out 2 ordered arrays + /// 1 of deposits underweight with most underweight placed first + /// 2 of deposits overweight with most overweight placed first + /// create an array of actions + /// iterate through list 2, starting at item 0, then iterate over list 1, making an action + /// pulling funds from this venue either till exhausted, or until item in list one is filled + /// if list 1 item filled, iterate to next item in list 1 + /// if list 1 item unfilled and list 2 item perfectly balanced, go to next item in list 2 + /// iterate over array of actions and correct decimals if swapping between USDC and DAI + /// @notice return what the perfectly balanced system would look like with all balances normalized to 1e18 function getExpectedPCVAmounts() - public + external view returns (PCVDepositInfo[] memory deposits) { @@ -428,11 +465,8 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { destinationAsset ); + /// if nothing is staked on source, ignore balance check if (sharesToVcon(source, venueTotalShares[source]) > 0) { - /// TODO simplify this by passing delta of starting balance - /// instead of calling balance again on each pcv deposit - /// record how balanced the system is before the PCV movement - int256 sourceVenueBalanceAfter = getVenueDeviation( source, totalPcv, @@ -455,7 +489,7 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { totalVconStaked ); - /// validate destination venue balance became more balanced if VCON is staked on it + /// validate destination venue balance became more balanced _checkBalance( destinationVenueBalance, destinationVenueBalanceAfter, @@ -492,12 +526,11 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { IPCVDepositV2(venue).accrue(); - uint256 endingLastRecordedProfit = IPCVDepositV2(venue) - .lastRecordedProfit(); /// think through calling the pcv oracle here + uint256 lastRecordedProfit = IPCVDepositV2(venue).lastRecordedProfit(); uint256 endingLastRecordedSharePrice = venueLastRecordedVconSharePrice[ venue ]; - int256 venueProfit = (endingLastRecordedProfit.toInt256() - + int256 venueProfit = (lastRecordedProfit.toInt256() - startingLastRecordedProfit.toInt256()); require(venueProfit >= 0, "MarketGovernance: loss scenario"); @@ -527,13 +560,9 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { } /// update the venue's profit index - venueLastRecordedProfit[venue] = endingLastRecordedProfit.toUint128(); + venueLastRecordedProfit[venue] = lastRecordedProfit.toUint128(); - emit VenueIndexUpdated( - venue, - block.timestamp, - endingLastRecordedProfit - ); + emit VenueIndexUpdated(venue, block.timestamp, lastRecordedProfit); } /// ---------- Governor-Only Permissioned API ---------- @@ -572,13 +601,13 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { /// @param venue where funds of denomination `token` will be withdrawn to /// @dev venue must be a whitelisted PCV Deposit, /// and the venue's underlying token must match the token passed - function setUnderlyingTokenHolderDeposit( + function setUnderlyingTokenHoldingDeposit( address token, address venue ) external onlyGovernor { require(pcvOracle().isVenue(venue), "MarketGovernance: invalid venue"); require( - IPCVDeposit(venue).balanceReportedIn() == token, + IPCVDepositV2(venue).balanceReportedIn() == token, "MarketGovernance: underlying mismatch" ); @@ -586,4 +615,39 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { emit UnderlyingTokenDepositUpdated(token, venue); } + + /// Governor applies losses to a venue + /// @param venue address of venue to apply losses to + /// @param newSharePrice new share price + function applyVenueLosses( + address venue, + uint128 newSharePrice + ) external onlyGovernor globalLock(1) { + require(pcvOracle().isVenue(venue), "MarketGovernance: invalid venue"); + + uint256 oldSharePrice = venueLastRecordedVconSharePrice[venue]; + + /// setting to 0 would cause re-initialization in accrue function, nullifying these changes + require( + newSharePrice != 0, + "MarketGovernance: cannot set share price to 0" + ); + + /// cannot apply a gain to the share price, only losses + require( + newSharePrice < oldSharePrice, + "MarketGovernance: share price not less" + ); + + IPCVDepositV2(venue).accrue(); + + uint256 lastRecordedProfit = IPCVDepositV2(venue).lastRecordedProfit(); + + /// update the venue's profit index + venueLastRecordedProfit[venue] = lastRecordedProfit.toUint128(); + /// update the venue's share price + venueLastRecordedVconSharePrice[venue] = newSharePrice; + + emit LossesApplied(venue, oldSharePrice, newSharePrice); + } } From 095b3c9f0a9e87bbb372adcd3fbc8c6867ebba29 Mon Sep 17 00:00:00 2001 From: Elliot Date: Thu, 2 Feb 2023 11:56:35 -0800 Subject: [PATCH 51/74] feedback --- src/vcon/IMarketGovernance.sol | 10 +++--- src/vcon/MarketGovernance.sol | 45 +++++++++++++-------------- test/unit/vcon/MarketGovernance.t.sol | 2 ++ 3 files changed, 28 insertions(+), 29 deletions(-) diff --git a/src/vcon/IMarketGovernance.sol b/src/vcon/IMarketGovernance.sol index d2b7a957e..27c512e6a 100644 --- a/src/vcon/IMarketGovernance.sol +++ b/src/vcon/IMarketGovernance.sol @@ -102,18 +102,18 @@ interface IMarketGovernance { /// stake VCON on a venue /// @param amountVcon to stake - /// @param source pcv deposit to pull funds from - function stake(uint256 amountVcon, address source) external; + /// @param venue pcv deposit to pull funds from + function stake(uint256 amountVcon, address venue) external; /// @notice this function automatically calculates - /// the amount of PCV to remove from the source + /// the amount of PCV to remove from the venue /// based on the user's total amount of staked VCON /// @param amountVcon to stake - /// @param source pcv deposit to pull funds from + /// @param venue pcv deposit to pull funds from /// @param vconRecipient address to receive VCON tokens function unstake( uint256 amountVcon, - address source, + address venue, address vconRecipient ) external; diff --git a/src/vcon/MarketGovernance.sol b/src/vcon/MarketGovernance.sol index 5cc90569c..89d8cbda0 100644 --- a/src/vcon/MarketGovernance.sol +++ b/src/vcon/MarketGovernance.sol @@ -113,57 +113,54 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { /// @notice a user can get slashed up to their full VCON stake for entering /// a venue that takes a loss. /// @param amountVcon to stake on destination - /// @param destination address to accrue rewards to, and send funds to + /// @param venue address to accrue rewards to, and send funds to function stake( uint256 amountVcon, - address destination + address venue ) external globalLock(1) whenNotPaused { IPCVOracle oracle = pcvOracle(); - require( - oracle.isVenue(destination), - "MarketGovernance: invalid destination" - ); + require(oracle.isVenue(venue), "MarketGovernance: invalid destination"); - _accrue(destination); /// update share price in the destination so the user gets in at the current share price + _accrue(venue); /// update share price in the destination so the user gets in at the current share price /// vconToShares will always return correctly as accrue will set venueLastRecordedVconSharePrice /// to the correct share price from 0 if uninitialized - uint256 userShareAmount = vconToShares(destination, amountVcon); + uint256 userShareAmount = vconToShares(venue, amountVcon); /// user updates - venueUserShares[destination][msg.sender] += userShareAmount; + venueUserShares[venue][msg.sender] += userShareAmount; /// venue updates - venueTotalShares[destination] += userShareAmount; + venueTotalShares[venue] += userShareAmount; /// check and an interaction with a trusted contract vcon().safeTransferFrom(msg.sender, address(this), amountVcon); /// transfer VCON in - emit Staked(destination, msg.sender, amountVcon); + emit Staked(venue, msg.sender, amountVcon); } /// @notice unstake VCON and transfer corresponding VCON to another venue /// @param shareAmount the amount of shares to unstake - /// @param source address to accrue rewards to, and pull funds from + /// @param venue address to accrue rewards to, and pull funds from /// @param vconRecipient address to receive the VCON - /// @dev both source and destination are checked twice, + /// @dev both venue and destination are checked twice, /// the first time in the market governance contract, /// the second time in the PCV Router contract. function unstake( uint256 shareAmount, - address source, + address venue, address vconRecipient ) external globalLock(1) whenNotPaused { /// ---------- Checks ---------- IPCVOracle oracle = pcvOracle(); - require(oracle.isVenue(source), "MarketGovernance: invalid source"); + require(oracle.isVenue(venue), "MarketGovernance: invalid venue"); require( - venueUserShares[source][msg.sender] >= shareAmount, + venueUserShares[venue][msg.sender] >= shareAmount, "MarketGovernance: invalid share amount" ); - address denomination = IPCVDepositV2(source).balanceReportedIn(); + address denomination = IPCVDepositV2(venue).balanceReportedIn(); address destination = underlyingTokenToHoldingDeposit[denomination]; require( destination != address(0), @@ -172,24 +169,24 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { /// ---------- Effects ---------- - _accrue(source); /// update profitPerVCON in the source so the user gets paid out at the current share price + _accrue(venue); /// update profitPerVCON in the venue so the user gets paid out at the current share price /// figure out how balanced the system is before withdraw /// amount of PCV to withdraw is the amount vcon * venue balance / total vcon staked on venue - uint256 amountPcv = getProRataPCVAmounts(source, shareAmount); + uint256 amountPcv = getProRataPCVAmounts(venue, shareAmount); /// user updates - venueUserShares[source][msg.sender] -= shareAmount; + venueUserShares[venue][msg.sender] -= shareAmount; /// venue updates - venueTotalShares[source] -= shareAmount; + venueTotalShares[venue] -= shareAmount; /// ---------- Interactions ---------- { PCVRouter(pcvRouter).movePCV( - source, + venue, destination, address(0), amountPcv, @@ -199,9 +196,9 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { } { - uint256 amountVcon = sharesToVcon(source, shareAmount); + uint256 amountVcon = sharesToVcon(venue, shareAmount); vcon().safeTransfer(vconRecipient, amountVcon); /// transfer VCON amount to recipient - emit Unstaked(source, msg.sender, amountVcon, amountPcv); + emit Unstaked(venue, msg.sender, amountVcon, amountPcv); } } diff --git a/test/unit/vcon/MarketGovernance.t.sol b/test/unit/vcon/MarketGovernance.t.sol index 5d865dd0b..283830b7d 100644 --- a/test/unit/vcon/MarketGovernance.t.sol +++ b/test/unit/vcon/MarketGovernance.t.sol @@ -609,6 +609,8 @@ contract UnitTestMarketGovernance is SystemUnitTest { assertTrue(pcvDepositUsdc.balance() < 2); } + /// TODO add rebalancing tests + function testStakeAndApplyLosses( DepositInfo[15] memory users, uint8 shareDenominator From 319371c945499dd7f14f4405628bd1ae43906f18 Mon Sep 17 00:00:00 2001 From: Elliot <34463580+ElliotFriedman@users.noreply.github.com> Date: Fri, 10 Feb 2023 09:19:12 -0800 Subject: [PATCH 52/74] PCV Oracle Refactor (#195) * checkpoint * fix unit tests * fix failing integration tests * fix failing unit tests * unit tests fixed * rename functions * remove getTotalLastRecordedPcv * update tests * increase test coverage * add pcv oracle hook to mock, increase unit test coverage * add additional tests and fix existing ones * import ordering * update mock and fix units * PCV Guardian todo * remove outdated todo * oracle invalid comment * oracle interface addition * Global Reentrancy Modifier Changes (#196) * remove reentrancy checks when lock is set to address 0 * add lock tests * add additional reentrancy tests when lock is set to 0 * update comments --- src/oracle/IPCVOracle.sol | 10 +- src/oracle/PCVOracle.sol | 90 +++-- src/refs/CoreRefV2.sol | 22 +- src/vcon/MarketGovernance.sol | 8 +- ...IntegrationTestCompoundBadDebtSentinel.sol | 16 + test/mock/MockCoreRefV2.sol | 11 + test/mock/MockPCVDepositV3.sol | 30 +- test/unit/core/GlobalReentrancyLock.t.sol | 6 +- test/unit/entry/SystemEntry.t.sol | 11 +- test/unit/oracle/PCVOracle.t.sol | 341 +++++++++++------- test/unit/pcv/PCVRouter.t.sol | 7 + test/unit/refs/CoreRefV2.t.sol | 33 +- test/unit/system/System.t.sol | 19 +- test/unit/vcon/MarketGovernance.t.sol | 4 +- 14 files changed, 407 insertions(+), 201 deletions(-) diff --git a/src/oracle/IPCVOracle.sol b/src/oracle/IPCVOracle.sol index 817f83d14..58037c6d9 100644 --- a/src/oracle/IPCVOracle.sol +++ b/src/oracle/IPCVOracle.sol @@ -42,14 +42,16 @@ interface IPCVOracle { /// @return boolean whether or not the venue is part of the venue list function isVenue(address venue) external view returns (bool); - /// @notice get the total PCV balance by looping through the pcv deposits - /// @dev this function is meant to be used offchain, as it is pretty gas expensive. - function getTotalPcv() external view returns (uint256 totalPcv); + /// @notice return the decimal normalized last recorded balance for venue + function lastRecordedPCV(address venue) external view returns (uint256); + + /// @notice return the decimal normalized last recorded profit for venue + function lastRecordedProfit(address venue) external view returns (int128); /// @notice get the total PCV balance by looping through the pcv deposits /// @dev this function is meant to be used offchain, as it is pretty gas expensive. /// this is an unsafe operation as it does not enforce the system is in an unlocked state - function getTotalPcvUnsafe() external view returns (uint256 totalPcv); + function getTotalPcv() external view returns (uint256 totalPcv); /// @notice returns decimal normalized version of a given venues USD balance function getVenueBalance(address venue) external view returns (uint256); diff --git a/src/oracle/PCVOracle.sol b/src/oracle/PCVOracle.sol index 536050baf..683d8bb03 100644 --- a/src/oracle/PCVOracle.sol +++ b/src/oracle/PCVOracle.sol @@ -6,9 +6,9 @@ import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet import {Constants} from "@voltprotocol/Constants.sol"; import {IOracleV2} from "@voltprotocol/oracle/IOracleV2.sol"; -import {IPCVOracle} from "@voltprotocol/oracle/IPCVOracle.sol"; import {VoltRoles} from "@voltprotocol/core/VoltRoles.sol"; import {CoreRefV2} from "@voltprotocol/refs/CoreRefV2.sol"; +import {IPCVOracle} from "@voltprotocol/oracle/IPCVOracle.sol"; import {IPCVDepositV2} from "@voltprotocol/pcv/IPCVDepositV2.sol"; /// @notice Contract to centralize information about PCV in the Volt system. @@ -28,6 +28,20 @@ contract PCVOracle is IPCVOracle, CoreRefV2 { /// know the USD value of PCV deployed in a given venue. mapping(address => address) public venueToOracle; + /// @notice track the venue's profit and losses + struct VenueData { + /// @notice the decimal normalized last recorded balance of PCV Deposit + int128 lastRecordedPCV; + /// @notice the decimal normalized last recorded profit of PCV Deposit + int128 lastRecordedProfit; + } + + /// @notice venue information, balance and profit + mapping(address => VenueData) public venueRecord; + + /// @notice cached total PCV amount + uint256 public lastRecordedTotalPcv; + ///@notice set of pcv deposit addresses EnumerableSet.AddressSet private venues; @@ -53,40 +67,35 @@ contract PCVOracle is IPCVOracle, CoreRefV2 { return venues.contains(venue); } - /// @notice get the total PCV balance by looping through the pcv deposits - /// @dev this function is meant to be used offchain, as it is pretty gas expensive. - function getTotalPcv() external view returns (uint256 totalPcv) { - require( - globalReentrancyLock().isUnlocked(), - "PCVOracle: cannot read while entered" - ); + /// @notice return the decimal normalized last recorded balance for venue + function lastRecordedPCV(address venue) public view returns (uint256) { + return venueRecord[venue].lastRecordedPCV.toUint256(); /// venue balance must be > -1, else safecast reverts + } - totalPcv = _getTotalPcv(); + /// @notice return the decimal normalized last recorded profit for venue + function lastRecordedProfit(address venue) public view returns (int128) { + return venueRecord[venue].lastRecordedProfit; } /// @notice get the total PCV balance by looping through the pcv deposits /// @dev this function is meant to be used offchain, as it is pretty gas expensive. /// this is an unsafe operation as it does not enforce the system is in an unlocked state - function getTotalPcvUnsafe() external view returns (uint256 totalPcv) { - totalPcv = _getTotalPcv(); - } - - /// @notice get the total PCV balance by looping through the pcv deposits - function _getTotalPcv() private view returns (uint256 totalPcv) { + function getTotalPcv() external view returns (uint256 totalPcv) { uint256 venueLength = venues.length(); - /// there will never be more than 100 total venues - /// so keep the math unchecked to save on gas - unchecked { - for (uint256 i = 0; i < venueLength; i++) { - address depositAddress = venues.at(i); - (uint256 oracleValue, bool oracleValid) = IOracleV2( - venueToOracle[depositAddress] - ).read(); - require(oracleValid, "PCVOracle: invalid oracle value"); - - uint256 balance = IPCVDepositV2(depositAddress).balance(); - totalPcv += (oracleValue * balance) / Constants.ETH_GRANULARITY; + for (uint256 i = 0; i < venueLength; ) { + address depositAddress = venues.at(i); + (uint256 oracleValue, bool oracleValid) = IOracleV2( + venueToOracle[depositAddress] + ).read(); + require(oracleValid, "PCVOracle: invalid oracle value"); + + uint256 balance = IPCVDepositV2(depositAddress).balance(); + totalPcv += (oracleValue * balance) / Constants.ETH_GRANULARITY; + /// there will never be more than 100 total venues + /// keep iteration math unchecked to save on gas + unchecked { + i++; } } } @@ -192,7 +201,7 @@ contract PCVOracle is IPCVOracle, CoreRefV2 { uint256 balance = IPCVDepositV2(venuesToAdd[i]).accrue(); if (balance != 0) { - // no need for safe cast here because balance is always > 0 + // no need for safe cast here because balance is always > 0 and < int256 max _readOracleAndUpdateAccounting( venuesToAdd[i], int256(balance), @@ -275,6 +284,24 @@ contract PCVOracle is IPCVOracle, CoreRefV2 { int256 deltaBalanceUSD, int256 deltaProfitUSD ) private { + VenueData storage ptr = venueRecord[venue]; + + int128 newLastRecordedPCV = ptr.lastRecordedPCV + + deltaBalanceUSD.toInt128(); + int128 newLastRecordedProfit = ptr.lastRecordedProfit + + deltaProfitUSD.toInt128(); + + /// single SSTORE + ptr.lastRecordedPCV = newLastRecordedPCV; + ptr.lastRecordedProfit = newLastRecordedProfit; + + /// update lastRecordedTotalPcv + if (deltaBalanceUSD >= 0) { + lastRecordedTotalPcv += deltaBalanceUSD.toUint256(); + } else { + lastRecordedTotalPcv -= (-deltaBalanceUSD).toUint256(); + } + // Emit event emit PCVUpdated( venue, @@ -282,13 +309,6 @@ contract PCVOracle is IPCVOracle, CoreRefV2 { deltaBalanceUSD, deltaProfitUSD ); - - // @dev: - // Later, we could store accumulative balances and profits - // for each venues here, in storage if needed by market governance. - // For now to save on gas, we only emit events. - // The PCVOracle can easily be swapped to a new implementation - // by calling setPCVOracle() on Core. } function _addVenue(address venue) private { diff --git a/src/refs/CoreRefV2.sol b/src/refs/CoreRefV2.sol index 1af26f795..307066923 100644 --- a/src/refs/CoreRefV2.sol +++ b/src/refs/CoreRefV2.sol @@ -33,17 +33,29 @@ abstract contract CoreRefV2 is ICoreRefV2, Pausable { /// 3. call core and unlock the lock back to starting level modifier globalLock(uint8 level) { IGlobalReentrancyLock lock = globalReentrancyLock(); - lock.lock(level); - _; - lock.unlock(level - 1); + + if (address(lock) != address(0)) { + lock.lock(level); + _; + lock.unlock(level - 1); + } else { + _; /// if lock is not set, allow function execution without global reentrancy locks + } } /// @notice modifier to restrict function acces to a certain lock level modifier isGlobalReentrancyLocked(uint8 level) { IGlobalReentrancyLock lock = globalReentrancyLock(); - require(lock.lockLevel() == level, "CoreRef: System not at lock level"); - _; + if (address(lock) != address(0)) { + require( + lock.lockLevel() == level, + "CoreRef: System not at lock level" + ); + _; + } else { + _; /// if lock is not set, allow function execution without global lock level + } } /// @notice callable only by the Volt Minter diff --git a/src/vcon/MarketGovernance.sol b/src/vcon/MarketGovernance.sol index 89d8cbda0..dbe202ef8 100644 --- a/src/vcon/MarketGovernance.sol +++ b/src/vcon/MarketGovernance.sol @@ -81,10 +81,10 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { mapping(address => uint256) public venueTotalShares; /// @notice map an underlying token to the corresponding holding deposit + /// TODO remove this and all related logic once the PCV Guardian is re-written + /// instead add a reference to the PCV Guardian mapping(address => address) public underlyingTokenToHoldingDeposit; - /// no balance checks when unstaking - /// ---------- Per Venue User Profit Tracking ---------- /// @dev convention for all double nested address mappings is key (venue -> user) -> value @@ -212,7 +212,7 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { ) external globalLock(1) whenNotPaused { /// read unsafe because we are at lock level 1 IPCVOracle oracle = pcvOracle(); - uint256 totalPcv = oracle.getTotalPcvUnsafe(); + uint256 totalPcv = oracle.getTotalPcv(); uint256 totalVconStaked = getTotalVconStaked(); unchecked { for (uint256 i = 0; i < movements.length; i++) { @@ -523,7 +523,7 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { IPCVDepositV2(venue).accrue(); - uint256 lastRecordedProfit = IPCVDepositV2(venue).lastRecordedProfit(); + uint256 lastRecordedProfit = IPCVDepositV2(venue).lastRecordedProfit(); /// get this from the pcv oracle as it will be decimal normalized uint256 endingLastRecordedSharePrice = venueLastRecordedVconSharePrice[ venue ]; diff --git a/test/integration/post-proposal-checks/IntegrationTestCompoundBadDebtSentinel.sol b/test/integration/post-proposal-checks/IntegrationTestCompoundBadDebtSentinel.sol index bcc2fe496..df298cf95 100644 --- a/test/integration/post-proposal-checks/IntegrationTestCompoundBadDebtSentinel.sol +++ b/test/integration/post-proposal-checks/IntegrationTestCompoundBadDebtSentinel.sol @@ -1,14 +1,18 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.13; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {PCVOracle} from "@voltprotocol/oracle/PCVOracle.sol"; import {PCVGuardian} from "@voltprotocol/pcv/PCVGuardian.sol"; import {IPCVDepositV2} from "@voltprotocol/pcv/IPCVDepositV2.sol"; import {PostProposalCheck} from "@test/integration/post-proposal-checks/PostProposalCheck.sol"; import {CompoundBadDebtSentinel} from "@voltprotocol/pcv/compound/CompoundBadDebtSentinel.sol"; contract IntegrationTestCompoundBadDebtSentinel is PostProposalCheck { + using SafeCast for *; + function testBadDebtOverThresholdAllowsSentinelWithdraw() public { CompoundBadDebtSentinel badDebtSentinel = CompoundBadDebtSentinel( addresses.mainnet("COMPOUND_BAD_DEBT_SENTINEL") @@ -16,6 +20,7 @@ contract IntegrationTestCompoundBadDebtSentinel is PostProposalCheck { PCVGuardian pcvGuardian = PCVGuardian( addresses.mainnet("PCV_GUARDIAN") ); + PCVOracle pcvOracle = PCVOracle(addresses.mainnet("PCV_ORACLE")); IPCVDepositV2 daiDeposit = IPCVDepositV2( addresses.mainnet("PCV_DEPOSIT_MORPHO_DAI") ); @@ -32,6 +37,17 @@ contract IntegrationTestCompoundBadDebtSentinel is PostProposalCheck { address[] memory user = new address[](1); user[0] = yearn; + assertApproxEq( + int256(daiDeposit.balance()), + pcvOracle.lastRecordedPCV(address(daiDeposit)).toInt256(), + 0 + ); + assertApproxEq( + int256(usdcDeposit.balance()) * 1e12, + pcvOracle.lastRecordedPCV(address(usdcDeposit)).toInt256(), + 0 + ); + assertTrue(badDebtSentinel.getTotalBadDebt(user) > 10_000_000e18); badDebtSentinel.rescueAllFromCompound(user); diff --git a/test/mock/MockCoreRefV2.sol b/test/mock/MockCoreRefV2.sol index f3eae9ed5..7df173352 100644 --- a/test/mock/MockCoreRefV2.sol +++ b/test/mock/MockCoreRefV2.sol @@ -17,6 +17,17 @@ contract MockCoreRefV2 is CoreRefV2 { function testSystemState() public onlyVoltRole(VoltRoles.LOCKER) {} + function testSystemLocksToLevel1() public globalLock(1) {} + + function testSystemLocksToLevel2() public globalLock(2) {} + + /// invalid lock level, doesn't matter because the lock is disabled in test + function testSystemLocksToLevel3() public globalLock(3) {} + + function testSystemLockLevel1() public isGlobalReentrancyLocked(1) {} + + function testSystemLockLevel2() public isGlobalReentrancyLocked(2) {} + function testStateGovernorMinter() public hasAnyOfThreeRoles( diff --git a/test/mock/MockPCVDepositV3.sol b/test/mock/MockPCVDepositV3.sol index 73235ea54..44344003a 100644 --- a/test/mock/MockPCVDepositV3.sol +++ b/test/mock/MockPCVDepositV3.sol @@ -2,11 +2,15 @@ pragma solidity 0.8.13; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import {CoreRefV2} from "@voltprotocol/refs/CoreRefV2.sol"; import {IPCVDeposit} from "@voltprotocol/pcv/IPCVDeposit.sol"; +import {IGlobalReentrancyLock} from "@voltprotocol/core/IGlobalReentrancyLock.sol"; contract MockPCVDepositV3 is IPCVDeposit, CoreRefV2 { + using SafeCast for *; + address public override balanceReportedIn; bool public checkPCVController = false; @@ -29,8 +33,24 @@ contract MockPCVDepositV3 is IPCVDeposit, CoreRefV2 { resistantProtocolOwnedVolt = _resistantProtocolOwnedVolt; } - function setLastRecordedProfit(uint256 _lastRecordedProfit) public { - lastRecordedProfit = _lastRecordedProfit; + function setLastRecordedProfit(int256 _lastRecordedProfit) public { + IGlobalReentrancyLock lock = globalReentrancyLock(); + if (address(lock) != address(0)) { + /// pcv oracle hook requires lock level 2 + lock.lock(1); + lock.lock(2); + } + + int256 deltaProfit = _lastRecordedProfit - + lastRecordedProfit.toInt256(); + _pcvOracleHook(deltaProfit, deltaProfit); /// balance increases with profits + + lastRecordedProfit = _lastRecordedProfit.toUint256(); + + if (address(lock) != address(0)) { + lock.unlock(1); + lock.unlock(0); + } } function setCheckPCVController(bool value) public { @@ -53,12 +73,17 @@ contract MockPCVDepositV3 is IPCVDeposit, CoreRefV2 { function accrue() external globalLock(2) returns (uint256) { uint256 _balance = balance(); resistantBalance = _balance; + + _pcvOracleHook(0, 0); + return _balance; } // IPCVDeposit V1 function deposit() external override globalLock(2) { + int256 startingBalance = resistantBalance.toInt256(); resistantBalance = IERC20(balanceReportedIn).balanceOf(address(this)); + _pcvOracleHook(resistantBalance.toInt256() - startingBalance, 0); } function withdraw( @@ -74,6 +99,7 @@ contract MockPCVDepositV3 is IPCVDeposit, CoreRefV2 { } IERC20(balanceReportedIn).transfer(to, amount); resistantBalance = IERC20(balanceReportedIn).balanceOf(address(this)); + _pcvOracleHook(-(amount.toInt256().toInt128()), 0); /// update balance with delta } function withdrawERC20( diff --git a/test/unit/core/GlobalReentrancyLock.t.sol b/test/unit/core/GlobalReentrancyLock.t.sol index 06bc2699b..e04de95ec 100644 --- a/test/unit/core/GlobalReentrancyLock.t.sol +++ b/test/unit/core/GlobalReentrancyLock.t.sol @@ -49,8 +49,8 @@ contract UnitTestGlobalReentrancyLock is Test { assertTrue(core.isGovernor(address(core))); /// core contract is governor assertTrue(core.isGovernor(addresses.governorAddress)); /// msg.sender of contract is governor - assertTrue(lock.isUnlocked()); /// core starts out unlocked - assertTrue(!lock.isLocked()); /// core starts out not locked + assertTrue(lock.isUnlocked()); /// lock starts out unlocked + assertTrue(!lock.isLocked()); /// lock starts out not locked assertEq(lock.lastSender(), address(0)); assertEq(lock.lastBlockEntered(), 0); @@ -219,7 +219,7 @@ contract UnitTestGlobalReentrancyLock is Test { assertTrue(!lock.isLocked()); assertEq(lock.lockLevel(), 0); - assertTrue(lock.isUnlocked()); /// core is fully unlocked + assertTrue(lock.isUnlocked()); /// lock is fully unlocked assertEq(toPrank, lock.lastSender()); vm.roll(block.number + 1); diff --git a/test/unit/entry/SystemEntry.t.sol b/test/unit/entry/SystemEntry.t.sol index 29f977890..4cb9f54c5 100644 --- a/test/unit/entry/SystemEntry.t.sol +++ b/test/unit/entry/SystemEntry.t.sol @@ -54,6 +54,9 @@ contract SystemEntryUnitTest is Test { token1 = new MockERC20(); deposit1 = new MockPCVDepositV3(address(core), address(token1)); + /// setup oracle + oracle1.setValues(1e18, true); + /// grant roles, set global reentrancy lock vm.startPrank(addresses.governorAddress); @@ -63,18 +66,20 @@ contract SystemEntryUnitTest is Test { core.setGlobalReentrancyLock(lock); - vm.stopPrank(); + core.createRole(VoltRoles.PCV_DEPOSIT, VoltRoles.GOVERNOR); + core.grantRole(VoltRoles.PCV_DEPOSIT, address(deposit1)); // setup pcv oracle address[] memory venues = new address[](1); venues[0] = address(deposit1); address[] memory oracles = new address[](1); oracles[0] = address(oracle1); - vm.prank(addresses.governorAddress); pcvOracle.addVenues(venues, oracles); + // setup pcv oracle - vm.prank(addresses.governorAddress); core.setPCVOracle(IPCVOracle(address(pcvOracle))); + + vm.stopPrank(); } function testSetup() public { diff --git a/test/unit/oracle/PCVOracle.t.sol b/test/unit/oracle/PCVOracle.t.sol index bee7999be..2f87f9835 100644 --- a/test/unit/oracle/PCVOracle.t.sol +++ b/test/unit/oracle/PCVOracle.t.sol @@ -1,10 +1,12 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.13; +import {stdError} from "@forge-std/StdError.sol"; import {Test} from "@forge-std/Test.sol"; + import {CoreV2} from "@voltprotocol/core/CoreV2.sol"; -import {IPCVOracle} from "@voltprotocol/oracle/IPCVOracle.sol"; import {PCVOracle} from "@voltprotocol/oracle/PCVOracle.sol"; +import {IPCVOracle} from "@voltprotocol/oracle/IPCVOracle.sol"; import {MockERC20} from "@test/mock/MockERC20.sol"; import {VoltRoles} from "@voltprotocol/core/VoltRoles.sol"; import {getCoreV2} from "@test/unit/utils/Fixtures.sol"; @@ -25,9 +27,11 @@ contract PCVOracleUnitTest is Test { // test Tokens MockERC20 private token1; MockERC20 private token2; + // test PCV Deposits MockPCVDepositV3 private deposit1; MockPCVDepositV3 private deposit2; + // test Oracles MockOracleV2 private oracle1; MockOracleV2 private oracle2; @@ -71,27 +75,51 @@ contract PCVOracleUnitTest is Test { core.grantLocker(address(pcvOracle)); core.grantLocker(address(deposit1)); core.grantLocker(address(deposit2)); + core.setPCVOracle(IPCVOracle(address(pcvOracle))); + + core.createRole(VoltRoles.PCV_DEPOSIT, VoltRoles.GOVERNOR); + core.grantRole(VoltRoles.PCV_DEPOSIT, address(deposit1)); + core.grantRole(VoltRoles.PCV_DEPOSIT, address(deposit2)); + + oracle1.setValues(1e18, true); + oracle2.setValues(1e18, true); + + // add venues + address[] memory venues = new address[](2); + venues[0] = address(deposit1); + venues[1] = address(deposit2); + address[] memory oracles = new address[](2); + oracles[0] = address(oracle1); + oracles[1] = address(oracle2); + pcvOracle.addVenues(venues, oracles); + vm.stopPrank(); } function testSetup() public { - assertEq(pcvOracle.getVenues().length, 0); + assertEq(pcvOracle.getVenues().length, 2); + assertEq(pcvOracle.getNumVenues(), 2); uint256 totalPcv = pcvOracle.getTotalPcv(); assertEq(totalPcv, 0); - } + assertEq(pcvOracle.lastRecordedTotalPcv(), 0); + + { + (int128 balance, int128 profit) = pcvOracle.venueRecord( + address(deposit1) + ); + assertEq(balance, 0); + assertEq(profit, 0); + } + { + (int128 balance, int128 profit) = pcvOracle.venueRecord( + address(deposit2) + ); + assertEq(balance, 0); + assertEq(profit, 0); + } - function testGetTotalPcvFailsWhileEntered() public { - vm.prank(address(deposit2)); - lock.lock(1); - - vm.expectRevert("PCVOracle: cannot read while entered"); - pcvOracle.getTotalPcv(); - - vm.prank(address(deposit2)); - lock.lock(2); - - vm.expectRevert("PCVOracle: cannot read while entered"); - pcvOracle.getTotalPcv(); + assertEq(pcvOracle.venueToOracle(address(deposit1)), address(oracle1)); + assertEq(pcvOracle.venueToOracle(address(deposit2)), address(oracle2)); } // ------------------------------------------------- @@ -99,25 +127,12 @@ contract PCVOracleUnitTest is Test { // ------------------------------------------------- function testSetVenueOracle() public { - assertEq(pcvOracle.venueToOracle(address(deposit1)), address(0)); + assertEq(pcvOracle.venueToOracle(address(deposit1)), address(oracle1)); // make deposit1 non-empty token1.mint(address(deposit1), 100e18); entry.deposit(address(deposit1)); - // set oracle value to 1$ - oracle1.setValues(1e18, true); - - // add venue - address[] memory venues = new address[](1); - venues[0] = address(deposit1); - address[] memory oracles = new address[](1); - oracles[0] = address(oracle1); - vm.prank(addresses.governorAddress); - pcvOracle.addVenues(venues, oracles); - - assertEq(pcvOracle.venueToOracle(address(deposit1)), address(oracle1)); - // create new oracle MockOracleV2 newOracle = new MockOracleV2(); newOracle.setValues(0.5e18, true); @@ -143,8 +158,8 @@ contract PCVOracleUnitTest is Test { function testSetVenueOracleRevertIfDepositDoesntExist() public { vm.prank(addresses.governorAddress); - vm.expectRevert(bytes("PCVOracle: invalid venue")); - pcvOracle.setVenueOracle(address(deposit1), address(oracle1)); + vm.expectRevert("PCVOracle: invalid venue"); + pcvOracle.setVenueOracle(address(10000), address(oracle1)); } function testSetVenueOracleRevertIfOracleInvalid() public { @@ -154,19 +169,6 @@ contract PCVOracleUnitTest is Test { token2.mint(address(deposit2), 100e18); entry.deposit(address(deposit2)); - oracle1.setValues(1e18, true); - oracle2.setValues(1e18, true); - - // add deposits in PCVOracle - address[] memory venues = new address[](2); - venues[0] = address(deposit1); - venues[1] = address(deposit2); - address[] memory oracles = new address[](2); - oracles[0] = address(oracle1); - oracles[1] = address(oracle2); - vm.prank(addresses.governorAddress); - pcvOracle.addVenues(venues, oracles); - // create new oracle MockOracleV2 newOracle = new MockOracleV2(); @@ -196,6 +198,9 @@ contract PCVOracleUnitTest is Test { oracles[0] = address(oracle1); oracles[1] = address(oracle2); + vm.prank(addresses.governorAddress); + pcvOracle.removeVenues(venues); + // pre-add check assertEq(pcvOracle.isVenue(address(deposit1)), false); assertEq(pcvOracle.isVenue(address(deposit2)), false); @@ -228,7 +233,8 @@ contract PCVOracleUnitTest is Test { assertEq(pcvOracle.isVenue(address(deposit1)), true); assertEq(pcvOracle.isVenue(address(deposit2)), true); assertEq(pcvOracle.venueToOracle(address(deposit1)), address(oracle1)); - assertEq(pcvOracle.venueToOracle(address(deposit2)), address(oracle2)); + assertEq(pcvOracle.venueToOracle(address(deposit1)), address(oracle1)); + assertEq(pcvOracle.getTotalPcv(), pcvOracle.lastRecordedTotalPcv()); } function testRemoveVenues() public { @@ -242,10 +248,6 @@ contract PCVOracleUnitTest is Test { oracles[0] = address(oracle1); oracles[1] = address(oracle2); - // add - vm.prank(addresses.governorAddress); - pcvOracle.addVenues(venues, oracles); - // pre-remove check assertEq(pcvOracle.isVenue(address(deposit1)), true); assertEq(pcvOracle.isVenue(address(deposit2)), true); @@ -288,17 +290,21 @@ contract PCVOracleUnitTest is Test { token1.mint(address(deposit1), 100e18); entry.deposit(address(deposit1)); - // set invalid oracle - oracle1.setValues(1e18, false); - // prepare add address[] memory venues = new address[](1); venues[0] = address(deposit1); address[] memory oracles = new address[](1); oracles[0] = address(oracle1); + + vm.prank(addresses.governorAddress); + pcvOracle.removeVenues(venues); + + // set invalid oracle + oracle1.setValues(1e18, false); + // add vm.prank(addresses.governorAddress); - vm.expectRevert(bytes("PCVOracle: invalid oracle value")); + vm.expectRevert("PCVOracle: invalid oracle value"); pcvOracle.addVenues(venues, oracles); } @@ -310,111 +316,146 @@ contract PCVOracleUnitTest is Test { token1.mint(address(deposit1), 100e18); entry.deposit(address(deposit1)); - // set valid oracle - oracle1.setValues(1e18, true); - - // prepare add - address[] memory venues = new address[](1); - venues[0] = address(deposit1); - address[] memory oracles = new address[](1); - oracles[0] = address(oracle1); - // add - vm.prank(addresses.governorAddress); - pcvOracle.addVenues(venues, oracles); - - // set invalid oracle - oracle1.setValues(1e18, false); - - // remove - vm.prank(addresses.governorAddress); - vm.expectRevert(bytes("PCVOracle: invalid oracle value")); - pcvOracle.removeVenues(venues); + { + (int128 balance, int128 profit) = pcvOracle.venueRecord( + address(deposit1) + ); + assertEq(balance, 100e18); + assertEq(profit, 0); + } + + { + address[] memory venues = new address[](2); + venues[0] = address(deposit1); + venues[1] = address(deposit2); + address[] memory oracles = new address[](2); + oracles[0] = address(oracle1); + oracles[1] = address(oracle2); + + vm.prank(addresses.governorAddress); + pcvOracle.removeVenues(venues); + } + + { + // prepare add + address[] memory venues = new address[](1); + venues[0] = address(deposit1); + address[] memory oracles = new address[](1); + oracles[0] = address(oracle1); + + // add + vm.prank(addresses.governorAddress); + pcvOracle.addVenues(venues, oracles); + + // set invalid oracle + oracle1.setValues(1e18, false); + + // remove + vm.prank(addresses.governorAddress); + vm.expectRevert("PCVOracle: invalid oracle value"); + pcvOracle.removeVenues(venues); + } } - // ---------------- Access Control ----------------- + // ------------------------------------------------- + // Accounting Checks + // ------------------------------------------------- - function testSetVenueOracleAcl() public { - vm.expectRevert(bytes("CoreRef: Caller is not a governor")); - pcvOracle.setVenueOracle(address(deposit1), address(oracle1)); + function testTotalPcvNegativeReverts() public { + vm.expectRevert(stdError.arithmeticError); + deposit1.setLastRecordedProfit(-1); } - function testAddVenuesAcl() public { - address[] memory venues = new address[](1); - address[] memory oracles = new address[](1); + function testProfitTracking() public { + int128 venue1Profit = 10e18; + int128 venue2Profit = 20e18; + + deposit1.setLastRecordedProfit(venue1Profit); + deposit2.setLastRecordedProfit(venue2Profit); + + { + (int128 balance, int128 profit) = pcvOracle.venueRecord( + address(deposit1) + ); + assertEq(balance, venue1Profit); + assertEq(profit, venue1Profit); + } + { + (int128 balance, int128 profit) = pcvOracle.venueRecord( + address(deposit2) + ); + assertEq(balance, venue2Profit); + assertEq(profit, venue2Profit); + } - vm.expectRevert(bytes("CoreRef: Caller is not a governor")); - pcvOracle.addVenues(venues, oracles); + assertEq( + pcvOracle.lastRecordedTotalPcv(), + uint256(uint128(venue1Profit + venue2Profit)) + ); /// profits are part of total PCV } - function testRemoveVenuesAcl() public { - address[] memory venues = new address[](1); - - vm.expectRevert(bytes("CoreRef: Caller is not a governor")); - pcvOracle.removeVenues(venues); + function testGetVenueBalanceRevertsInvalidOracle() public { + oracle1.setValues(oracle1.price(), false); + vm.expectRevert("PCVOracle: invalid oracle value"); + pcvOracle.getVenueBalance(address(deposit1)); } - // ------------------------------------------------- - // Accounting Checks - // ------------------------------------------------- - function testTrackDepositValueOnAddAndRemove() public { + { + address[] memory venues = new address[](1); + venues[0] = address(deposit2); + vm.prank(addresses.governorAddress); + pcvOracle.removeVenues(venues); + } + // make deposit1 non-empty token1.mint(address(deposit1), 100e18); entry.deposit(address(deposit1)); - // set oracle values - oracle1.setValues(1e18, true); // simulating "DAI" (1$ coin, 18 decimals) - - // add venue - address[] memory venues = new address[](1); - venues[0] = address(deposit1); - address[] memory oracles = new address[](1); - oracles[0] = address(oracle1); - vm.prank(addresses.governorAddress); - pcvOracle.addVenues(venues, oracles); + assertEq(pcvOracle.lastRecordedPCV(address(deposit1)), 100e18); + assertEq( + pcvOracle.lastRecordedPCV(address(deposit1)), + pcvOracle.getVenueBalance(address(deposit1)) + ); + assertEq(pcvOracle.lastRecordedProfit(address(deposit1)), 0); // check getPcv() uint256 totalPcv1 = pcvOracle.getTotalPcv(); assertEq(totalPcv1, 100e18); // 100$ total - // remove venue - vm.prank(addresses.governorAddress); - pcvOracle.removeVenues(venues); + { + address[] memory venues = new address[](1); + venues[0] = address(deposit1); + // remove venue + vm.prank(addresses.governorAddress); + pcvOracle.removeVenues(venues); + } + + assertEq(pcvOracle.lastRecordedPCV(address(deposit1)), 0); + assertEq(pcvOracle.lastRecordedProfit(address(deposit1)), 0); + + vm.expectRevert("PCVOracle: invalid caller deposit"); + pcvOracle.getVenueBalance(address(deposit1)); // check getPcv() uint256 totalPcv2 = pcvOracle.getTotalPcv(); assertEq(totalPcv2, 0); // 0$ total + assertEq(uint256(pcvOracle.lastRecordedTotalPcv()), totalPcv2); } function testAccountingOnHook() public { - // make deposit1 non-empty - token1.mint(address(deposit1), 100e18); - entry.deposit(address(deposit1)); - // set oracle values oracle1.setValues(1e18, true); // simulating "DAI" (1$ coin, 18 decimals) oracle2.setValues(1e18 * 1e12, true); // simulating "USDC" (1$ coin, 6 decimals) - // add venues - address[] memory venues = new address[](2); - venues[0] = address(deposit1); - venues[1] = address(deposit2); - address[] memory oracles = new address[](2); - oracles[0] = address(oracle1); - oracles[1] = address(oracle2); - vm.prank(addresses.governorAddress); - pcvOracle.addVenues(venues, oracles); + // make deposit1 non-empty + token1.mint(address(deposit1), 100e18); + entry.deposit(address(deposit1)); // check getPcv() uint256 totalPcv1 = pcvOracle.getTotalPcv(); assertEq(totalPcv1, 100e18); // 100$ total - - // grant roles to pcv deposits - vm.startPrank(addresses.governorAddress); - core.createRole(VoltRoles.PCV_DEPOSIT, VoltRoles.GOVERNOR); - core.grantRole(VoltRoles.PCV_DEPOSIT, address(deposit1)); - core.grantRole(VoltRoles.PCV_DEPOSIT, address(deposit2)); - vm.stopPrank(); + assertEq(uint256(pcvOracle.lastRecordedTotalPcv()), totalPcv1); // deposit 1 has 100$ + 300$ token1.mint(address(deposit1), 300e18); @@ -425,7 +466,9 @@ contract PCVOracleUnitTest is Test { // check getPcv() uint256 totalPcv2 = pcvOracle.getTotalPcv(); + assertEq(totalPcv2, 800e18); // 800$ total + assertEq(uint256(pcvOracle.lastRecordedTotalPcv()), totalPcv2); // A call from a PCVDeposit refreshes accounting vm.startPrank(address(deposit2)); @@ -452,22 +495,12 @@ contract PCVOracleUnitTest is Test { // set oracle values oracle1.setValues(123456, true); // oracle valid - // add venues - address[] memory venues = new address[](2); - venues[0] = address(deposit1); - venues[1] = address(deposit2); - address[] memory oracles = new address[](2); - oracles[0] = address(oracle1); - oracles[1] = address(oracle2); - vm.prank(addresses.governorAddress); - pcvOracle.addVenues(venues, oracles); - // set oracle values oracle1.setValues(123456, false); // oracle invalid oracle2.setValues(1e18, true); // getPcv() reverts because oracle is invalid - vm.expectRevert(bytes("PCVOracle: invalid oracle value")); + vm.expectRevert("PCVOracle: invalid oracle value"); pcvOracle.getTotalPcv(); // set oracle values @@ -475,14 +508,50 @@ contract PCVOracleUnitTest is Test { oracle2.setValues(123456, false); // oracle invalid // getPcv() reverts because oracle is invalid - vm.expectRevert(bytes("PCVOracle: invalid oracle value")); + vm.expectRevert("PCVOracle: invalid oracle value"); pcvOracle.getTotalPcv(); } + function testPcvHookFailsInvalidOracleValue() public { + // set oracle values + oracle1.setValues(123456, false); // oracle invalid + + vm.startPrank(address(deposit1)); + + lock.lock(1); + lock.lock(2); + + // getPcv() reverts because oracle is invalid + vm.expectRevert("PCVOracle: invalid oracle value"); + pcvOracle.updateBalance(0, 0); /// 0 delta balance or profit + + vm.stopPrank(); + } + // ---------------- Access Control ----------------- + function testSetVenueOracleAcl() public { + vm.expectRevert("CoreRef: Caller is not a governor"); + pcvOracle.setVenueOracle(address(deposit1), address(oracle1)); + } + + function testAddVenuesAcl() public { + address[] memory venues = new address[](1); + address[] memory oracles = new address[](1); + + vm.expectRevert("CoreRef: Caller is not a governor"); + pcvOracle.addVenues(venues, oracles); + } + + function testRemoveVenuesAcl() public { + address[] memory venues = new address[](1); + + vm.expectRevert("CoreRef: Caller is not a governor"); + pcvOracle.removeVenues(venues); + } + function testUpdateBalanceAcl() public { - vm.expectRevert(bytes("UNAUTHORIZED")); + vm.expectRevert("UNAUTHORIZED"); pcvOracle.updateBalance(0.5e18, 0); } diff --git a/test/unit/pcv/PCVRouter.t.sol b/test/unit/pcv/PCVRouter.t.sol index aa8bc41a3..3526c1734 100644 --- a/test/unit/pcv/PCVRouter.t.sol +++ b/test/unit/pcv/PCVRouter.t.sol @@ -105,6 +105,13 @@ contract PCVRouterUnitTest is Test { core.createRole(VoltRoles.PCV_MOVER, VoltRoles.GOVERNOR); core.grantRole(VoltRoles.PCV_MOVER, address(this)); core.setGlobalReentrancyLock(lock); + + core.createRole(VoltRoles.PCV_DEPOSIT, VoltRoles.GOVERNOR); + core.grantRole(VoltRoles.PCV_DEPOSIT, address(depositToken1A)); + core.grantRole(VoltRoles.PCV_DEPOSIT, address(depositToken1B)); + core.grantRole(VoltRoles.PCV_DEPOSIT, address(depositToken2A)); + core.grantRole(VoltRoles.PCV_DEPOSIT, address(depositToken2B)); + vm.stopPrank(); // setup deposits diff --git a/test/unit/refs/CoreRefV2.t.sol b/test/unit/refs/CoreRefV2.t.sol index aa4f778cc..ea8f3c60a 100644 --- a/test/unit/refs/CoreRefV2.t.sol +++ b/test/unit/refs/CoreRefV2.t.sol @@ -6,10 +6,11 @@ import {Test} from "@forge-std/Test.sol"; import {ICoreV2} from "@voltprotocol/core/ICoreV2.sol"; import {VoltRoles} from "@voltprotocol/core/VoltRoles.sol"; import {MockERC20} from "@test/mock/MockERC20.sol"; +import {getCoreV2} from "@test/unit/utils/Fixtures.sol"; import {IPCVOracle} from "@voltprotocol/oracle/IPCVOracle.sol"; import {MockCoreRefV2} from "@test/mock/MockCoreRefV2.sol"; import {TestAddresses as addresses} from "@test/unit/utils/TestAddresses.sol"; -import {getCoreV2} from "@test/unit/utils/Fixtures.sol"; +import {IGlobalReentrancyLock} from "@voltprotocol/core/IGlobalReentrancyLock.sol"; contract UnitTestCoreRefV2 is Test { ICoreV2 private core; @@ -113,6 +114,36 @@ contract UnitTestCoreRefV2 is Test { coreRef.testSystemState(); } + function _disableLock() private { + vm.prank(addresses.governorAddress); + core.setGlobalReentrancyLock(IGlobalReentrancyLock(address(0))); + } + + function testLockLevel1LockDisabled() public { + _disableLock(); + coreRef.testSystemLocksToLevel1(); + } + + function testLockLevel2LockDisabled() public { + _disableLock(); + coreRef.testSystemLocksToLevel2(); + } + + function testLockLevel3LockDisabled() public { + _disableLock(); + coreRef.testSystemLocksToLevel3(); + } + + function testSystemLockLevel1LockDisabled() public { + _disableLock(); + coreRef.testSystemLockLevel1(); + } + + function testSystemLockLevel2LockDisabled() public { + _disableLock(); + coreRef.testSystemLockLevel2(); + } + function testGuardianAsGuardian() public { vm.prank(addresses.guardianAddress); coreRef.testGuardian(); diff --git a/test/unit/system/System.t.sol b/test/unit/system/System.t.sol index afe54196b..907b88598 100644 --- a/test/unit/system/System.t.sol +++ b/test/unit/system/System.t.sol @@ -267,6 +267,13 @@ contract SystemUnitTest is Test { allocator.connectDeposit(address(usdcpsm), address(pcvDepositUsdc)); allocator.connectDeposit(address(daipsm), address(pcvDepositDai)); + /// top up contracts with tokens for testing + /// if done after the setting of pcv oracle, balances will be incorrect unless deposit is called + dai.mint(address(daipsm), daiTargetBalance); + usdc.mint(address(usdcpsm), usdcTargetBalance); + dai.mint(address(pcvDepositDai), daiTargetBalance); + usdc.mint(address(pcvDepositUsdc), usdcTargetBalance); + /// Configure PCV Oracle address[] memory venues = new address[](2); venues[0] = address(pcvDepositDai); @@ -280,13 +287,11 @@ contract SystemUnitTest is Test { core.setPCVOracle(pcvOracle); - vm.stopPrank(); + core.createRole(VoltRoles.PCV_DEPOSIT, VoltRoles.GOVERNOR); + core.grantRole(VoltRoles.PCV_DEPOSIT, address(pcvDepositDai)); + core.grantRole(VoltRoles.PCV_DEPOSIT, address(pcvDepositUsdc)); - /// top up contracts with tokens for testing - dai.mint(address(daipsm), daiTargetBalance); - usdc.mint(address(usdcpsm), usdcTargetBalance); - dai.mint(address(pcvDepositDai), daiTargetBalance); - usdc.mint(address(pcvDepositUsdc), usdcTargetBalance); + vm.stopPrank(); vm.label(address(timelockController), "Timelock Controller"); vm.label(address(entry), "entry"); @@ -428,6 +433,8 @@ contract SystemUnitTest is Test { } function testPCVGuardWithdrawAllToSafeAddress() public { + entry.deposit(address(pcvDepositDai)); + entry.deposit(address(pcvDepositUsdc)); vm.startPrank(addresses.userAddress); pcvGuardian.withdrawAllToSafeAddress(address(daipsm)); diff --git a/test/unit/vcon/MarketGovernance.t.sol b/test/unit/vcon/MarketGovernance.t.sol index 283830b7d..a3c18b90d 100644 --- a/test/unit/vcon/MarketGovernance.t.sol +++ b/test/unit/vcon/MarketGovernance.t.sol @@ -692,7 +692,7 @@ contract UnitTestMarketGovernance is SystemUnitTest { totalShares; pcvDepositDai.setLastRecordedProfit( - pcvDepositDai.lastRecordedProfit() + periodGain + (pcvDepositDai.lastRecordedProfit() + periodGain).toInt256() ); mgov.accrueVcon(address(pcvDepositDai)); @@ -834,7 +834,7 @@ contract UnitTestMarketGovernance is SystemUnitTest { } function testUnstakeInvalidSourceVenueFails() public { - vm.expectRevert("MarketGovernance: invalid source"); + vm.expectRevert("MarketGovernance: invalid venue"); mgov.unstake(0, address(0), address(0)); } From d51db0ea8fabe2168a39cd5c706568a9b86fe031 Mon Sep 17 00:00:00 2001 From: Elliot Date: Fri, 17 Feb 2023 15:41:55 -0800 Subject: [PATCH 53/74] address Erwan PCV Deposit comments --- src/pcv/ERC20Allocator.sol | 28 ++++++------ src/pcv/IPCVDepositV2.sol | 13 +++--- src/pcv/PCVDepositV2.sol | 44 ++++++++----------- src/pcv/PCVRouter.sol | 14 +++--- src/pcv/morpho/MorphoAavePCVDeposit.sol | 12 ++--- src/pcv/morpho/MorphoCompoundPCVDeposit.sol | 5 --- src/peg/BasePegStabilityModule.sol | 5 +++ src/peg/INonCustodialPSM.sol | 8 ++-- src/peg/NonCustodialPSM.sol | 16 +++---- src/vcon/MarketGovernance.sol | 9 ++-- ...egrationTestMorphoCompoundPCVDeposit.t.sol | 11 ++--- .../IntegrationTestEulerPCVDeposit.sol | 36 --------------- .../IntegrationTestMorphoAavePCVDeposit.sol | 36 --------------- test/mock/MockPSM.sol | 10 ++--- test/proposals/Addresses.sol | 4 +- test/proposals/vips/vip16.sol | 11 ++--- test/unit/peg/UnitTestNonCustodialPSM.t.sol | 18 ++++---- 17 files changed, 95 insertions(+), 185 deletions(-) diff --git a/src/pcv/ERC20Allocator.sol b/src/pcv/ERC20Allocator.sol index e791c634e..dfcf8b1b0 100644 --- a/src/pcv/ERC20Allocator.sol +++ b/src/pcv/ERC20Allocator.sol @@ -7,7 +7,7 @@ import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {CoreRefV2} from "@voltprotocol/refs/CoreRefV2.sol"; -import {PCVDeposit} from "@voltprotocol/pcv/PCVDeposit.sol"; +import {IPCVDepositV2} from "@voltprotocol/pcv/IPCVDepositV2.sol"; import {IERC20Allocator} from "@voltprotocol/pcv/IERC20Allocator.sol"; /// @notice Contract to remove all excess funds past a target balance from a smart contract @@ -66,7 +66,7 @@ contract ERC20Allocator is IERC20Allocator, CoreRefV2 { uint248 psmTargetBalance, int8 decimalsNormalizer ) external override onlyGovernor { - address token = PCVDeposit(psm).balanceReportedIn(); + address token = IPCVDepositV2(psm).token(); require( allPSMs[psm].token == address(0), @@ -92,7 +92,7 @@ contract ERC20Allocator is IERC20Allocator, CoreRefV2 { address psm, uint248 psmTargetBalance ) external override onlyGovernor { - address token = PCVDeposit(psm).balanceReportedIn(); + address token = IPCVDepositV2(psm).token(); address storedToken = allPSMs[psm].token; require( storedToken != address(0), @@ -129,7 +129,7 @@ contract ERC20Allocator is IERC20Allocator, CoreRefV2 { /// assert pcv deposit and psm share same denomination require( - PCVDeposit(pcvDeposit).balanceReportedIn() == pcvToken, + IPCVDepositV2(pcvDeposit).token() == pcvToken, "ERC20Allocator: token mismatch" ); require(pcvToken != address(0), "ERC20Allocator: invalid underlying"); @@ -186,10 +186,10 @@ contract ERC20Allocator is IERC20Allocator, CoreRefV2 { /// pull funds from pull target and send to push target /// automatically pulls underlying token - PCVDeposit(psm).withdraw(pcvDeposit, amountToSkim); + IPCVDepositV2(psm).withdraw(pcvDeposit, amountToSkim); /// deposit pulled funds into the selected yield venue - PCVDeposit(pcvDeposit).deposit(); + IPCVDepositV2(pcvDeposit).deposit(); emit Skimmed(amountToSkim, pcvDeposit); } @@ -201,13 +201,13 @@ contract ERC20Allocator is IERC20Allocator, CoreRefV2 { address psm = pcvDepositToPSM[pcvDeposit]; require(psm != address(0), "ERC20Allocator: invalid PCVDeposit"); - _drip(psm, PCVDeposit(pcvDeposit)); + _drip(psm, IPCVDepositV2(pcvDeposit)); } /// helper function that does the dripping /// @param psm peg stability module to drip to /// @param pcvDeposit pcv deposit to pull funds from - function _drip(address psm, PCVDeposit pcvDeposit) internal { + function _drip(address psm, IPCVDepositV2 pcvDeposit) internal { /// Check require( _checkDripCondition(psm, pcvDeposit), @@ -239,8 +239,8 @@ contract ERC20Allocator is IERC20Allocator, CoreRefV2 { require(psm != address(0), "ERC20Allocator: invalid PCVDeposit"); /// don't check buffer != 0 as that will happen in drip function on effects - if (_checkDripCondition(psm, PCVDeposit(pcvDeposit))) { - _drip(psm, PCVDeposit(pcvDeposit)); + if (_checkDripCondition(psm, IPCVDepositV2(pcvDeposit))) { + _drip(psm, IPCVDepositV2(pcvDeposit)); } else if (_checkSkimCondition(psm)) { _skim(psm, pcvDeposit); } @@ -324,7 +324,7 @@ contract ERC20Allocator is IERC20Allocator, CoreRefV2 { /// amountToDrip = 1,000e6 amountToDrip = Math.min( - Math.min(targetBalanceDelta, PCVDeposit(pcvDeposit).balance()), + Math.min(targetBalanceDelta, IPCVDepositV2(pcvDeposit).balance()), /// adjust for decimals here as buffer is 1e18 scaled, /// and if token is not scaled by 1e18, then this amountToDrip could be over the buffer /// because buffer is 1e18 adjusted, and decimals normalizer is used to adjust up to the buffer @@ -355,7 +355,7 @@ contract ERC20Allocator is IERC20Allocator, CoreRefV2 { } address psm = pcvDepositToPSM[pcvDeposit]; - return _checkDripCondition(psm, PCVDeposit(pcvDeposit)); + return _checkDripCondition(psm, IPCVDepositV2(pcvDeposit)); } /// @notice function that returns whether the amount of tokens held @@ -386,13 +386,13 @@ contract ERC20Allocator is IERC20Allocator, CoreRefV2 { /// cannot drip with an empty buffer return (globalSystemExitRateLimiter().buffer() != 0 && - _checkDripCondition(psm, PCVDeposit(pcvDeposit))) || + _checkDripCondition(psm, IPCVDepositV2(pcvDeposit))) || _checkSkimCondition(psm); } function _checkDripCondition( address psm, - PCVDeposit pcvDeposit + IPCVDepositV2 pcvDeposit ) internal view returns (bool) { /// direct balanceOf call is cheaper than calling balance on psm /// also cannot drip if balance in underlying venue is 0 diff --git a/src/pcv/IPCVDepositV2.sol b/src/pcv/IPCVDepositV2.sol index 42151d31f..0abb375d2 100644 --- a/src/pcv/IPCVDepositV2.sol +++ b/src/pcv/IPCVDepositV2.sol @@ -55,9 +55,9 @@ interface IPCVDepositV2 { /// to the lastRecordedBalance variable function deposit() external; - /// @notice claim COMP rewards for supplying to Morpho. - /// Does not require reentrancy lock as no smart contract state is mutated - /// in this function. + /// @notice claim token rewards for supplying. + /// Does not require reentrancy lock as no internal smart + /// contract state is mutated in this function. function harvest() external; /// @notice function that emits an event tracking profits and losses @@ -69,13 +69,10 @@ interface IPCVDepositV2 { // ----------- Getters ----------- - /// @notice gets the effective balance of "balanceReportedIn" token if the deposit were fully withdrawn + /// @notice gets the effective balance of token if the deposit were fully withdrawn function balance() external view returns (uint256); - /// @notice gets the token address in which this deposit returns its balance - function balanceReportedIn() external view returns (address); - - /// @notice address of underlying token + /// @notice token address in which this deposit returns its balance function token() external view returns (address); /// @notice address of reward token diff --git a/src/pcv/PCVDepositV2.sol b/src/pcv/PCVDepositV2.sol index 321f8798e..dc5baa9d9 100644 --- a/src/pcv/PCVDepositV2.sol +++ b/src/pcv/PCVDepositV2.sol @@ -6,20 +6,22 @@ import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {ILens} from "@voltprotocol/pcv/morpho/ILens.sol"; -import {ICToken} from "@voltprotocol/pcv/morpho/ICompound.sol"; -import {IMorpho} from "@voltprotocol/pcv/morpho/IMorpho.sol"; import {CoreRefV2} from "@voltprotocol/refs/CoreRefV2.sol"; import {IPCVDepositV2} from "@voltprotocol/pcv/IPCVDepositV2.sol"; /// @title abstract PCV Deposit contract. -/// if PCV Oracle is not set, this contract will revert +/// if PCV Oracle is not set, this contract will revert and not allow operations +/// if global reentrancy lock is not set, this contract will still work. /// @author Elliot Friedman abstract contract PCVDepositV2 is IPCVDepositV2, CoreRefV2 { using SafeERC20 for IERC20; using SafeCast for *; - /// @notice reference to underlying token + /// ------------------------------------------ + /// --------------- Immutables --------------- + /// ------------------------------------------ + + /// @notice reference to reward token address public immutable override rewardToken; /// @notice reference to underlying token @@ -34,22 +36,13 @@ abstract contract PCVDepositV2 is IPCVDepositV2, CoreRefV2 { rewardToken = _rewardToken; } - /// ------------------------------------------ - /// -------------- View Only API ------------- - /// ------------------------------------------ - - /// @notice return the underlying token denomination for this deposit - function balanceReportedIn() external view returns (address) { - return address(token); - } - /// ------------------------------------------ /// ----------- Permissionless API ----------- /// ------------------------------------------ - /// @notice deposit ERC-20 tokens to Morpho-Compound - /// non-reentrant to block malicious reentrant state changes - /// to the lastRecordedBalance variable + /// @notice deposit ERC-20 tokens to underlying venue + /// uses global reentrancy lock to block malicious reentrant + /// state changes to the lastRecordedBalance variable function deposit() public whenNotPaused globalLock(2) { /// ------ Check ------ @@ -86,9 +79,9 @@ abstract contract PCVDepositV2 is IPCVDepositV2, CoreRefV2 { emit Deposit(msg.sender, amount); } - /// @notice claim COMP rewards for supplying to Morpho. - /// Does not require reentrancy lock as no smart contract state is mutated - /// in this function. + /// @notice claim token rewards for supplying to underlying venue. + /// Does not require reentrancy lock as no internal smart contract + /// state is mutated in this function. function harvest() external globalLock(2) { uint256 claimedAmount = _claim(); @@ -127,8 +120,8 @@ abstract contract PCVDepositV2 is IPCVDepositV2, CoreRefV2 { _pcvOracleHook(-(amount.toInt256()) + profit, profit); } - /// @notice withdraw all tokens from Morpho - /// non-reentrant as state changes and external calls are made + /// @notice withdraw all tokens from underlying venue + /// global reentrancy locked as state changes and external calls are made /// @param to the address PCV will be sent to function withdrawAll(address to) external onlyPCVController globalLock(2) { /// compute profit from interest accrued and emit an event @@ -197,7 +190,8 @@ abstract contract PCVDepositV2 is IPCVDepositV2, CoreRefV2 { } /// currentBalance should always be greater than or equal to - /// the deposited amount, except on the same block a deposit occurs, or a loss event in morpho + /// the deposited amount, except on the same block a deposit + /// occurs, or a loss event in underlying venue. /// Compute profit int256 profit = int256(currentBalance) - int256(lastRecordedBalance); @@ -211,10 +205,10 @@ abstract contract PCVDepositV2 is IPCVDepositV2, CoreRefV2 { /// @notice helper avoid repeated code in withdraw and withdrawAll /// anytime this function is called it is by an external function in this smart contract /// with a reentrancy guard. This ensures lastRecordedBalance never desynchronizes. - /// Morpho is assumed to be a loss-less venue. over the course of less than 1 block, + /// All current venues are assumed to be loss-less venues. Over the course of less than 1 block, /// it is possible to lose funds. However, after 1 block, deposits are expected to always /// be in profit at least with current interest rates around 0.8% natively on Compound, - /// ignoring all COMP and Morpho rewards. + /// ignoring all other token rewards. /// @param to recipient of withdraw funds /// @param amount to withdraw /// @param recordPnl whether or not to record PnL. Set to false in withdrawAll diff --git a/src/pcv/PCVRouter.sol b/src/pcv/PCVRouter.sol index 2770e80a7..06bea9e63 100644 --- a/src/pcv/PCVRouter.sol +++ b/src/pcv/PCVRouter.sol @@ -8,7 +8,7 @@ import {VoltRoles} from "@voltprotocol/core/VoltRoles.sol"; import {IPCVOracle} from "@voltprotocol/oracle/IPCVOracle.sol"; import {IPCVRouter} from "@voltprotocol/pcv/IPCVRouter.sol"; import {IPCVSwapper} from "@voltprotocol/pcv/IPCVSwapper.sol"; -import {IPCVDeposit} from "@voltprotocol/pcv/IPCVDeposit.sol"; +import {IPCVDepositV2} from "@voltprotocol/pcv/IPCVDepositV2.sol"; /// @title Volt Protocol PCV Router /// @notice A contract that allows PCV movements between deposits. @@ -117,11 +117,11 @@ contract PCVRouter is IPCVRouter, CoreRefV2 { // Check underlying tokens require( - IPCVDeposit(source).balanceReportedIn() == sourceAsset, + IPCVDepositV2(source).token() == sourceAsset, "PCVRouter: invalid source asset" ); require( - IPCVDeposit(destination).balanceReportedIn() == destinationAsset, + IPCVDepositV2(destination).token() == destinationAsset, "PCVRouter: invalid destination asset" ); // Check swapper, if applicable @@ -190,7 +190,7 @@ contract PCVRouter is IPCVRouter, CoreRefV2 { address sourceAsset, address destinationAsset ) external whenNotPaused onlyPCVController globalLock(1) { - uint256 amount = IPCVDeposit(source).balance(); + uint256 amount = IPCVDepositV2(source).balance(); _movePCV( source, destination, @@ -213,17 +213,17 @@ contract PCVRouter is IPCVRouter, CoreRefV2 { // Do transfer uint256 amountDestination; if (swapper != address(0)) { - IPCVDeposit(source).withdraw(swapper, amountSource); + IPCVDepositV2(source).withdraw(swapper, amountSource); amountDestination = IPCVSwapper(swapper).swap( sourceAsset, destinationAsset, destination ); } else { - IPCVDeposit(source).withdraw(destination, amountSource); + IPCVDepositV2(source).withdraw(destination, amountSource); amountDestination = amountSource; } - IPCVDeposit(destination).deposit(); + IPCVDepositV2(destination).deposit(); // Emit event emit PCVMovement(source, destination, amountSource, amountDestination); diff --git a/src/pcv/morpho/MorphoAavePCVDeposit.sol b/src/pcv/morpho/MorphoAavePCVDeposit.sol index 948e38b18..a5624a501 100644 --- a/src/pcv/morpho/MorphoAavePCVDeposit.sol +++ b/src/pcv/morpho/MorphoAavePCVDeposit.sol @@ -7,7 +7,6 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {ILens} from "@voltprotocol/pcv/morpho/ILens.sol"; import {IMorpho} from "@voltprotocol/pcv/morpho/IMorpho.sol"; import {CoreRefV2} from "@voltprotocol/refs/CoreRefV2.sol"; -import {Constants} from "@voltprotocol/Constants.sol"; import {PCVDepositV2} from "@voltprotocol/pcv/PCVDepositV2.sol"; /// @notice PCV Deposit for Morpho-Aave V2. @@ -19,11 +18,6 @@ import {PCVDepositV2} from "@voltprotocol/pcv/PCVDepositV2.sol"; /// and then calls supply on Morpho, which pulls the underlying token to Morpho, /// drawing down on the approved amount to be spent, /// and then giving this PCV Deposit credits on Morpho in exchange for the underlying -/// @dev PCV Guardian functions withdrawERC20ToSafeAddress and withdrawAllERC20ToSafeAddress -/// will not work with removing Morpho Tokens on the Morpho PCV Deposit because Morpho -/// has no concept of mTokens. This means if the contract is paused, or an issue is -/// surfaced in Morpho and liquidity is locked, Volt will need to rely on social -/// coordination with the Morpho team to recover funds. /// @dev Depositing and withdrawing in a single block will cause a very small loss /// of funds, less than a pip. The way to not realize this loss is by depositing and /// then withdrawing at least 1 block later. That way, interest accrues. @@ -55,8 +49,8 @@ contract MorphoAavePCVDeposit is PCVDepositV2 { /// @param _aToken aToken this deposit references /// @param _underlying Token denomination of this deposit /// @param _rewardToken Reward token denomination of this deposit - /// @param _morpho reference to the morpho-compound v2 market - /// @param _lens reference to the morpho-compound v2 lens + /// @param _morpho reference to the morpho-aave v2 market + /// @param _lens reference to the morpho-aave v2 lens constructor( address _core, address _aToken, @@ -74,7 +68,7 @@ contract MorphoAavePCVDeposit is PCVDepositV2 { /// ------------------ Views ----------------- /// ------------------------------------------ - /// @notice Returns the distribution of assets supplied by this contract through Morpho-Compound. + /// @notice Returns the distribution of assets supplied by this contract through Morpho-Aave. /// @return sum of suppliedP2P and suppliedOnPool for the given aToken function balance() public view override returns (uint256) { (, , uint256 totalSupplied) = ILens(lens).getCurrentSupplyBalanceInOf( diff --git a/src/pcv/morpho/MorphoCompoundPCVDeposit.sol b/src/pcv/morpho/MorphoCompoundPCVDeposit.sol index 0b36c34b9..a7d437a45 100644 --- a/src/pcv/morpho/MorphoCompoundPCVDeposit.sol +++ b/src/pcv/morpho/MorphoCompoundPCVDeposit.sol @@ -18,11 +18,6 @@ import {PCVDepositV2} from "@voltprotocol/pcv/PCVDepositV2.sol"; /// and then calls supply on Morpho, which pulls the underlying token to Morpho, /// drawing down on the approved amount to be spent, /// and then giving this PCV Deposit credits on Morpho in exchange for the underlying -/// @dev PCV Guardian functions withdrawERC20ToSafeAddress and withdrawAllERC20ToSafeAddress -/// will not work with removing Morpho Tokens on the Morpho PCV Deposit because Morpho -/// has no concept of mTokens. This means if the contract is paused, or an issue is -/// surfaced in Morpho and liquidity is locked, Volt will need to rely on social -/// coordination with the Morpho team to recover funds. /// @dev Depositing and withdrawing in a single block will cause a very small loss /// of funds, less than a pip. The way to not realize this loss is by depositing and /// then withdrawing at least 1 block later. That way, interest accrues. diff --git a/src/peg/BasePegStabilityModule.sol b/src/peg/BasePegStabilityModule.sol index d4876cfdd..6fd6beb7c 100644 --- a/src/peg/BasePegStabilityModule.sol +++ b/src/peg/BasePegStabilityModule.sol @@ -77,6 +77,11 @@ abstract contract BasePegStabilityModule is IPegStabilityModule, OracleRefV2 { /// ----------- Public View-Only API ---------- + /// @notice return address of token + function token() public view returns (address) { + return address(underlyingToken); + } + /// @notice calculate the amount of VOLT out for a given `amountIn` of underlying /// First get oracle price of token /// Then figure out how many dollars that amount in is worth by multiplying price * amount. diff --git a/src/peg/INonCustodialPSM.sol b/src/peg/INonCustodialPSM.sol index 51607bf9b..a1a96904e 100644 --- a/src/peg/INonCustodialPSM.sol +++ b/src/peg/INonCustodialPSM.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.13; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {IPCVDeposit} from "@voltprotocol/pcv/IPCVDeposit.sol"; +import {IPCVDepositV2} from "@voltprotocol/pcv/IPCVDepositV2.sol"; /** * @title Volt Peg Stability Module @@ -22,15 +22,15 @@ interface INonCustodialPSM { // ----------- Governor or Admin Only State Changing API ----------- /// @notice set the target for sending surplus reserves - function setPCVDeposit(IPCVDeposit newTarget) external; + function setPCVDeposit(IPCVDepositV2 newTarget) external; // ----------- Getters ----------- /// @notice the PCV deposit target to deposit and withdraw from - function pcvDeposit() external view returns (IPCVDeposit); + function pcvDeposit() external view returns (IPCVDepositV2); // ----------- Events ----------- /// @notice event emitted when surplus target is updated - event PCVDepositUpdate(IPCVDeposit oldTarget, IPCVDeposit newTarget); + event PCVDepositUpdate(address oldTarget, address newTarget); } diff --git a/src/peg/NonCustodialPSM.sol b/src/peg/NonCustodialPSM.sol index 905895b1f..aa4544ee0 100644 --- a/src/peg/NonCustodialPSM.sol +++ b/src/peg/NonCustodialPSM.sol @@ -8,7 +8,7 @@ import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {Constants} from "@voltprotocol/Constants.sol"; import {OracleRefV2} from "@voltprotocol/refs/OracleRefV2.sol"; -import {IPCVDeposit} from "@voltprotocol/pcv/IPCVDeposit.sol"; +import {IPCVDepositV2} from "@voltprotocol/pcv/IPCVDepositV2.sol"; import {INonCustodialPSM} from "@voltprotocol/peg/INonCustodialPSM.sol"; import {BasePegStabilityModule} from "@voltprotocol/peg/BasePegStabilityModule.sol"; @@ -25,7 +25,7 @@ contract NonCustodialPSM is BasePegStabilityModule, INonCustodialPSM { using SafeCast for *; /// @notice reference to the fully liquid venue redemptions can occur in - IPCVDeposit public pcvDeposit; + IPCVDepositV2 public pcvDeposit; /// @notice construct the PSM /// @param coreAddress reference to core @@ -45,7 +45,7 @@ contract NonCustodialPSM is BasePegStabilityModule, INonCustodialPSM { IERC20 underlyingTokenAddress, uint128 floorPrice, uint128 ceilingPrice, - IPCVDeposit pcvDepositAddress + IPCVDepositV2 pcvDepositAddress ) BasePegStabilityModule( coreAddress, @@ -67,7 +67,7 @@ contract NonCustodialPSM is BasePegStabilityModule, INonCustodialPSM { /// @param newTarget new PCV Deposit target for this PSM /// enforces that underlying on this PSM and new Deposit are the same function setPCVDeposit( - IPCVDeposit newTarget + IPCVDepositV2 newTarget ) external override onlyGovernor { _setPCVDeposit(newTarget); } @@ -193,14 +193,14 @@ contract NonCustodialPSM is BasePegStabilityModule, INonCustodialPSM { /// @notice helper function to set the PCV deposit /// @param newPCVDeposit the new PCV deposit that this PSM will pull assets from and deposit assets into - function _setPCVDeposit(IPCVDeposit newPCVDeposit) private { + function _setPCVDeposit(IPCVDepositV2 newPCVDeposit) private { require( - newPCVDeposit.balanceReportedIn() == address(underlyingToken), + newPCVDeposit.token() == address(underlyingToken), "PegStabilityModule: Underlying token mismatch" ); - IPCVDeposit oldTarget = pcvDeposit; + IPCVDepositV2 oldTarget = pcvDeposit; pcvDeposit = newPCVDeposit; - emit PCVDepositUpdate(oldTarget, newPCVDeposit); + emit PCVDepositUpdate(address(oldTarget), address(newPCVDeposit)); } } diff --git a/src/vcon/MarketGovernance.sol b/src/vcon/MarketGovernance.sol index fc5c9a541..d4fd91876 100644 --- a/src/vcon/MarketGovernance.sol +++ b/src/vcon/MarketGovernance.sol @@ -160,7 +160,7 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { "MarketGovernance: invalid share amount" ); - address denomination = IPCVDepositV2(venue).balanceReportedIn(); + address denomination = IPCVDepositV2(venue).token(); address destination = underlyingTokenToHoldingDeposit[denomination]; require( destination != address(0), @@ -448,9 +448,8 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { int256 destinationVenueBalance, uint256 totalVconStaked ) private { - address sourceAsset = IPCVDepositV2(source).balanceReportedIn(); - address destinationAsset = IPCVDepositV2(destination) - .balanceReportedIn(); + address sourceAsset = IPCVDepositV2(source).token(); + address destinationAsset = IPCVDepositV2(destination).token(); /// validate pcv movement /// check underlying assets match up and if not that swapper is provided and valid @@ -604,7 +603,7 @@ contract MarketGovernance is CoreRefV2, IMarketGovernance { ) external onlyGovernor { require(pcvOracle().isVenue(venue), "MarketGovernance: invalid venue"); require( - IPCVDepositV2(venue).balanceReportedIn() == token, + IPCVDepositV2(venue).token() == token, "MarketGovernance: underlying mismatch" ); diff --git a/test/integration/IntegrationTestMorphoCompoundPCVDeposit.t.sol b/test/integration/IntegrationTestMorphoCompoundPCVDeposit.t.sol index 8b0b6c6ac..161cbf10d 100644 --- a/test/integration/IntegrationTestMorphoCompoundPCVDeposit.t.sol +++ b/test/integration/IntegrationTestMorphoCompoundPCVDeposit.t.sol @@ -97,11 +97,8 @@ contract IntegrationTestMorphoCompoundPCVDeposit is Test { vm.label(0x930f1b46e1D081Ec1524efD95752bE3eCe51EF67, "Morpho Lens"); vm.label(0x8888882f8f843896699869179fB6E4f7e3B58888, "MORPHO_COMPOUND"); - //// todo remove this prank and use deal instead - vm.startPrank(DAI_USDC_USDT_CURVE_POOL); - dai.transfer(address(daiDeposit), targetDaiBalance); - usdc.transfer(address(usdcDeposit), targetUsdcBalance); - vm.stopPrank(); + deal(address(dai), address(daiDeposit), targetDaiBalance); + deal(address(usdc), address(usdcDeposit), targetUsdcBalance); vm.startPrank(addresses.governorAddress); @@ -147,8 +144,8 @@ contract IntegrationTestMorphoCompoundPCVDeposit is Test { assertEq(daiDeposit.lens(), MORPHO_LENS); assertEq(usdcDeposit.lens(), MORPHO_LENS); - assertEq(daiDeposit.balanceReportedIn(), address(dai)); - assertEq(usdcDeposit.balanceReportedIn(), address(usdc)); + assertEq(daiDeposit.token(), address(dai)); + assertEq(usdcDeposit.token(), address(usdc)); assertEq(address(daiDeposit.cToken()), address(CDAI)); assertEq(address(usdcDeposit.cToken()), address(CUSDC)); diff --git a/test/integration/post-proposal-checks/IntegrationTestEulerPCVDeposit.sol b/test/integration/post-proposal-checks/IntegrationTestEulerPCVDeposit.sol index 8ff97ccf2..a111d54c7 100644 --- a/test/integration/post-proposal-checks/IntegrationTestEulerPCVDeposit.sol +++ b/test/integration/post-proposal-checks/IntegrationTestEulerPCVDeposit.sol @@ -5,7 +5,6 @@ import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {console} from "@forge-std/console.sol"; -import {PCVGuardian} from "@voltprotocol/pcv/PCVGuardian.sol"; import {SystemEntry} from "@voltprotocol/entry/SystemEntry.sol"; import {IPCVDepositV2} from "@voltprotocol/pcv/IPCVDepositV2.sol"; import {EulerPCVDeposit} from "@voltprotocol/pcv/euler/EulerPCVDeposit.sol"; @@ -91,39 +90,4 @@ contract IntegrationTestEulerPCVDeposit is PostProposalCheck { 0 ); } - - function testWithdrawPCVGuardian() public { - testCanDepositEuler(); - - PCVGuardian pcvGuardian = PCVGuardian( - addresses.mainnet("PCV_GUARDIAN") - ); - - address[2] memory addressesToClean = [ - addresses.mainnet("PCV_DEPOSIT_EULER_DAI"), - addresses.mainnet("PCV_DEPOSIT_EULER_USDC") - ]; - - vm.startPrank(addresses.mainnet("GOVERNOR")); - for (uint256 i = 0; i < addressesToClean.length; i++) { - pcvGuardian.withdrawAllToSafeAddress(addressesToClean[i]); - // Check only dust left after withdrawals - assertLt(IPCVDepositV2(addressesToClean[i]).balance(), 1e6); - } - vm.stopPrank(); - - // sanity checks - address safeAddress = pcvGuardian.safeAddress(); - require(safeAddress != address(0), "Safe address is 0 address"); - assertApproxEq( - IERC20(addresses.mainnet("DAI")).balanceOf(safeAddress).toInt256(), - (depositAmount * 1e18).toInt256(), - 0 - ); // ~1M DAI - assertApproxEq( - IERC20(addresses.mainnet("USDC")).balanceOf(safeAddress).toInt256(), - (depositAmount * 1e6).toInt256(), - 0 - ); // ~1M USDC - } } diff --git a/test/integration/post-proposal-checks/IntegrationTestMorphoAavePCVDeposit.sol b/test/integration/post-proposal-checks/IntegrationTestMorphoAavePCVDeposit.sol index 74fd8faef..52bce09c4 100644 --- a/test/integration/post-proposal-checks/IntegrationTestMorphoAavePCVDeposit.sol +++ b/test/integration/post-proposal-checks/IntegrationTestMorphoAavePCVDeposit.sol @@ -5,7 +5,6 @@ import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {console} from "@forge-std/console.sol"; -import {PCVGuardian} from "@voltprotocol/pcv/PCVGuardian.sol"; import {SystemEntry} from "@voltprotocol/entry/SystemEntry.sol"; import {IPCVDepositV2} from "@voltprotocol/pcv/IPCVDepositV2.sol"; import {PostProposalCheck} from "@test/integration/post-proposal-checks/PostProposalCheck.sol"; @@ -95,39 +94,4 @@ contract IntegrationTestMorphoAavePCVDeposit is PostProposalCheck { 0 ); } - - function testWithdrawPCVGuardian() public { - testCanDepositAave(); - - PCVGuardian pcvGuardian = PCVGuardian( - addresses.mainnet("PCV_GUARDIAN") - ); - - address[2] memory addressesToClean = [ - addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_DAI"), - addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_USDC") - ]; - - vm.startPrank(addresses.mainnet("GOVERNOR")); - for (uint256 i = 0; i < addressesToClean.length; i++) { - pcvGuardian.withdrawAllToSafeAddress(addressesToClean[i]); - // Check only dust left after withdrawals - assertLt(IPCVDepositV2(addressesToClean[i]).balance(), 1e6); - } - vm.stopPrank(); - - // sanity checks - address safeAddress = pcvGuardian.safeAddress(); - require(safeAddress != address(0), "Safe address is 0 address"); - assertApproxEq( - IERC20(addresses.mainnet("DAI")).balanceOf(safeAddress).toInt256(), - (depositAmount * 1e18).toInt256(), - 0 - ); // ~1M DAI - assertApproxEq( - IERC20(addresses.mainnet("USDC")).balanceOf(safeAddress).toInt256(), - (depositAmount * 1e6).toInt256(), - 0 - ); // ~1M USDC - } } diff --git a/test/mock/MockPSM.sol b/test/mock/MockPSM.sol index f913a1b37..b45f8e0b4 100644 --- a/test/mock/MockPSM.sol +++ b/test/mock/MockPSM.sol @@ -1,17 +1,17 @@ pragma solidity =0.8.13; contract MockPSM { - address public underlying; + address public token; - constructor(address _underlying) { - underlying = _underlying; + constructor(address _token) { + token = _token; } function setUnderlying(address newUnderlying) external { - underlying = newUnderlying; + token = newUnderlying; } function balanceReportedIn() external view returns (address) { - return underlying; + return token; } } diff --git a/test/proposals/Addresses.sol b/test/proposals/Addresses.sol index 065cdef19..59fc1918c 100644 --- a/test/proposals/Addresses.sol +++ b/test/proposals/Addresses.sol @@ -64,8 +64,8 @@ contract Addresses is Test { // ---------- EULER ADDRESSES ---------- _addMainnet("EULER_MAIN", 0x27182842E098f60e3D576794A5bFFb0777E025d3); - _addMainnet("EUSDC", 0xEb91861f8A4e1C12333F42DCE8fB0Ecdc28dA716); - _addMainnet("EDAI", 0xe025E3ca2bE02316033184551D4d3Aa22024D9DC); + _addMainnet("EULER_USDC", 0xEb91861f8A4e1C12333F42DCE8fB0Ecdc28dA716); + _addMainnet("EULER_DAI", 0xe025E3ca2bE02316033184551D4d3Aa22024D9DC); // ---------- ORACLE ADDRESSES ---------- _addMainnet( diff --git a/test/proposals/vips/vip16.sol b/test/proposals/vips/vip16.sol index bd7334b4b..c10df748e 100644 --- a/test/proposals/vips/vip16.sol +++ b/test/proposals/vips/vip16.sol @@ -16,8 +16,10 @@ import {PCVRouter} from "@voltprotocol/pcv/PCVRouter.sol"; import {PCVOracle} from "@voltprotocol/oracle/PCVOracle.sol"; import {CoreRefV2} from "@voltprotocol/refs/CoreRefV2.sol"; import {IPCVOracle} from "@voltprotocol/oracle/IPCVOracle.sol"; +import {PCVDeposit} from "@voltprotocol/pcv/PCVDeposit.sol"; import {SystemEntry} from "@voltprotocol/entry/SystemEntry.sol"; import {PCVGuardian} from "@voltprotocol/pcv/PCVGuardian.sol"; +import {IPCVDepositV2} from "@voltprotocol/pcv/IPCVDepositV2.sol"; import {MigratorRouter} from "@voltprotocol/v1-migration/MigratorRouter.sol"; import {ERC20Allocator} from "@voltprotocol/pcv/ERC20Allocator.sol"; import {NonCustodialPSM} from "@voltprotocol/peg/NonCustodialPSM.sol"; @@ -30,7 +32,6 @@ import {PegStabilityModule} from "@voltprotocol/peg/PegStabilityModule.sol"; import {IPegStabilityModule} from "@voltprotocol/peg/IPegStabilityModule.sol"; import {ConstantPriceOracle} from "@voltprotocol/oracle/ConstantPriceOracle.sol"; import {CompoundBadDebtSentinel} from "@voltprotocol/pcv/compound/CompoundBadDebtSentinel.sol"; -import {IPCVDeposit, PCVDeposit} from "@voltprotocol/pcv/PCVDeposit.sol"; import {IVoltMigrator, VoltMigrator} from "@voltprotocol/v1-migration/VoltMigrator.sol"; import {IGlobalReentrancyLock, GlobalReentrancyLock} from "@voltprotocol/core/GlobalReentrancyLock.sol"; import {IGlobalRateLimitedMinter, GlobalRateLimitedMinter} from "@voltprotocol/rate-limits/GlobalRateLimitedMinter.sol"; @@ -212,7 +213,7 @@ contract vip16 is Proposal { /// Euler Deposits EulerPCVDeposit eulerDaiPCVDeposit = new EulerPCVDeposit( addresses.mainnet("CORE"), - addresses.mainnet("EDAI"), + addresses.mainnet("EULER_DAI"), addresses.mainnet("EULER_MAIN"), addresses.mainnet("DAI"), address(0) @@ -220,7 +221,7 @@ contract vip16 is Proposal { EulerPCVDeposit eulerUsdcPCVDeposit = new EulerPCVDeposit( addresses.mainnet("CORE"), - addresses.mainnet("EUSDC"), + addresses.mainnet("EULER_USDC"), addresses.mainnet("EULER_MAIN"), addresses.mainnet("USDC"), address(0) @@ -276,7 +277,7 @@ contract vip16 is Proposal { IERC20(addresses.mainnet("USDC")), VOLT_FLOOR_PRICE_USDC, VOLT_CEILING_PRICE_USDC, - IPCVDeposit( + IPCVDepositV2( addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_USDC") ) ); @@ -289,7 +290,7 @@ contract vip16 is Proposal { IERC20(addresses.mainnet("DAI")), VOLT_FLOOR_PRICE_DAI, VOLT_CEILING_PRICE_DAI, - IPCVDeposit( + IPCVDepositV2( addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_DAI") ) ); diff --git a/test/unit/peg/UnitTestNonCustodialPSM.t.sol b/test/unit/peg/UnitTestNonCustodialPSM.t.sol index 8c0e023f8..c89898512 100644 --- a/test/unit/peg/UnitTestNonCustodialPSM.t.sol +++ b/test/unit/peg/UnitTestNonCustodialPSM.t.sol @@ -13,8 +13,8 @@ import {VoltRoles} from "@voltprotocol/core/VoltRoles.sol"; import {MockERC20} from "@test/mock/MockERC20.sol"; import {PCVDeposit} from "@voltprotocol/pcv/PCVDeposit.sol"; import {PCVGuardian} from "@voltprotocol/pcv/PCVGuardian.sol"; -import {IPCVDeposit} from "@voltprotocol/pcv/IPCVDeposit.sol"; import {SystemEntry} from "@voltprotocol/entry/SystemEntry.sol"; +import {IPCVDepositV2} from "@voltprotocol/pcv/IPCVDepositV2.sol"; import {NonCustodialPSM} from "@voltprotocol/peg/NonCustodialPSM.sol"; import {MockPCVDepositV3} from "@test/mock/MockPCVDepositV3.sol"; import {VoltSystemOracle} from "@voltprotocol/oracle/VoltSystemOracle.sol"; @@ -59,7 +59,7 @@ interface IERC20Mintable is IERC20 { contract NonCustodialPSMUnitTest is Test { using SafeCast for *; - event PCVDepositUpdate(IPCVDeposit oldTarget, IPCVDeposit newPCVDeposit); + event PCVDepositUpdate(address oldTarget, address newPCVDeposit); CoreV2 private core; SystemEntry private entry; @@ -145,7 +145,7 @@ contract NonCustodialPSMUnitTest is Test { dai, voltFloorPrice, voltCeilingPrice, - IPCVDeposit(address(pcvDeposit)) + IPCVDepositV2(address(pcvDeposit)) ); custodialPsm = new PegStabilityModule( @@ -261,7 +261,7 @@ contract NonCustodialPSMUnitTest is Test { dai, voltFloorPrice, voltCeilingPrice, - IPCVDeposit(address(pcvDeposit)) + IPCVDepositV2(address(pcvDeposit)) ); assertEq(psm.getExitValue(amount), amount / (1e12)); @@ -277,7 +277,7 @@ contract NonCustodialPSMUnitTest is Test { dai, voltFloorPrice, voltCeilingPrice, - IPCVDeposit(address(pcvDeposit)) + IPCVDepositV2(address(pcvDeposit)) ); assertEq(psm.getExitValue(amount), uint256(amount) * (1e12)); @@ -501,11 +501,11 @@ contract NonCustodialPSMUnitTest is Test { } function testSetPCVDepositGovernorSucceeds() public { - IPCVDeposit newDeposit = IPCVDeposit( + IPCVDepositV2 newDeposit = IPCVDepositV2( address(new MockPCVDepositV3(coreAddress, address(dai))) ); vm.expectEmit(true, true, false, true, address(psm)); - emit PCVDepositUpdate(pcvDeposit, newDeposit); + emit PCVDepositUpdate(address(pcvDeposit), address(newDeposit)); vm.prank(addresses.governorAddress); psm.setPCVDeposit(newDeposit); @@ -513,7 +513,7 @@ contract NonCustodialPSMUnitTest is Test { } function testSetPCVDepositGovernorFailsMismatchUnderlyingToken() public { - IPCVDeposit newDeposit = IPCVDeposit( + IPCVDepositV2 newDeposit = IPCVDepositV2( address(new MockPCVDepositV3(coreAddress, address(12345))) ); vm.prank(addresses.governorAddress); @@ -530,7 +530,7 @@ contract NonCustodialPSMUnitTest is Test { function testSetPCVDepositNonGovernorFails() public { vm.expectRevert("CoreRef: Caller is not a governor"); - psm.setPCVDeposit(IPCVDeposit(address(0))); + psm.setPCVDeposit(IPCVDepositV2(address(0))); } function testSetOracleCeilingPriceNonGovernorFails() public { From 7a0181df9f3140e748ebe3d64e7a482f98c82321 Mon Sep 17 00:00:00 2001 From: Elliot Date: Fri, 17 Feb 2023 23:13:50 -0800 Subject: [PATCH 54/74] rate limit mid points, remove psm and allocator --- slither/slither-10-21-v2.txt | 2 +- src/core/CoreV2.sol | 18 - src/core/ICoreV2.sol | 13 - src/core/IPermissionsV2.sol | 48 +- src/core/PermissionsV2.sol | 240 ++--- src/core/VoltRoles.sol | 20 +- src/pcv/ERC20Allocator.sol | 411 -------- src/peg/BasePegStabilityModule.sol | 195 ---- src/peg/INonCustodialPSM.sol | 83 +- src/peg/IPegStabilityModule.sol | 29 - src/peg/NonCustodialPSM.sol | 203 +++- src/peg/PegStabilityModule.sol | 187 ---- src/rate-limits/GlobalRateLimitedMinter.sol | 13 +- .../GlobalSystemExitRateLimiter.sol | 65 -- .../IGlobalSystemExitRateLimiter.sol | 16 - src/refs/CoreRefV2.sol | 11 - src/refs/ICoreRefV2.sol | 1 - src/utils/IRateLimitedV2.sol | 12 +- src/utils/RateLimitedV2.sol | 74 +- ...egrationTestMorphoCompoundPCVDeposit.t.sol | 1 - .../IntegrationTestPCVOracle.sol | 8 +- .../IntegrationTestRateLimiters.sol | 161 +-- .../IntegrationTestRoles.sol | 129 +-- .../IntegrationTestUsePSMs.sol | 67 -- .../IntegrationTestVoltV1Migration.sol | 809 +++++++------- .../IntegrationTestProposalPSMOracle.sol | 6 +- test/mock/MockRateLimitedV2.sol | 4 +- test/proposals/vips/vip15.sol | 18 +- test/proposals/vips/vip16.sol | 211 +--- test/unit/core/CoreV2.t.sol | 27 - test/unit/core/PermissionsV2.t.sol | 81 +- .../limiter/GlobalRateLimitedMinter.t.sol | 18 +- .../limiter/GlobalSystemExitRateLimiter.t.sol | 158 --- test/unit/pcv/utils/ERC20Allocator.t.sol | 987 ------------------ .../pcv/utils/ERC20AllocatorConnector.t.sol | 264 ----- test/unit/peg/UnitTestNonCustodialPSM.t.sol | 107 +- .../unit/peg/UnitTestPegStabilityModule.t.sol | 517 --------- test/unit/system/System.t.sol | 236 +---- test/unit/utils/RateLimitedV2.t.sol | 57 +- 39 files changed, 984 insertions(+), 4523 deletions(-) delete mode 100644 src/pcv/ERC20Allocator.sol delete mode 100644 src/peg/BasePegStabilityModule.sol delete mode 100644 src/peg/PegStabilityModule.sol delete mode 100644 src/rate-limits/GlobalSystemExitRateLimiter.sol delete mode 100644 src/rate-limits/IGlobalSystemExitRateLimiter.sol delete mode 100644 test/integration/post-proposal-checks/IntegrationTestUsePSMs.sol delete mode 100644 test/unit/limiter/GlobalSystemExitRateLimiter.t.sol delete mode 100644 test/unit/pcv/utils/ERC20Allocator.t.sol delete mode 100644 test/unit/pcv/utils/ERC20AllocatorConnector.t.sol delete mode 100644 test/unit/peg/UnitTestPegStabilityModule.t.sol diff --git a/slither/slither-10-21-v2.txt b/slither/slither-10-21-v2.txt index eaee0cde9..7c74d4c46 100644 --- a/slither/slither-10-21-v2.txt +++ b/slither/slither-10-21-v2.txt @@ -96,7 +96,7 @@ Parameter PermissionsV2.isGovernor(address)._address (contracts/core/Permissions Parameter PermissionsV2.isGuardian(address)._address (contracts/core/PermissionsV2.sol#247) is not in mixedCase Parameter PermissionsV2.isLocker(address)._address (contracts/core/PermissionsV2.sol#255) is not in mixedCase Parameter PermissionsV2.isPCVGuard(address)._address (contracts/core/PermissionsV2.sol#262) is not in mixedCase -Parameter PermissionsV2.isRateLimitedMinter(address)._address (contracts/core/PermissionsV2.sol#270) is not in mixedCase +Parameter PermissionsV2.isPsmMinter(address)._address (contracts/core/PermissionsV2.sol#270) is not in mixedCase Parameter PermissionsV2.isRateLimitedRedeemer(address)._address (contracts/core/PermissionsV2.sol#279) is not in mixedCase Function IRateLimitedV2.MAX_RATE_LIMIT_PER_SECOND() (contracts/utils/IRateLimitedV2.sol#11) is not in mixedCase Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#conformance-to-solidity-naming-conventions diff --git a/src/core/CoreV2.sol b/src/core/CoreV2.sol index 9a5a51b38..5e9ab17f7 100644 --- a/src/core/CoreV2.sol +++ b/src/core/CoreV2.sol @@ -8,7 +8,6 @@ import {ICoreV2} from "@voltprotocol/core/ICoreV2.sol"; import {IPCVOracle} from "@voltprotocol/oracle/IPCVOracle.sol"; import {PermissionsV2} from "@voltprotocol/core/PermissionsV2.sol"; import {IGlobalRateLimitedMinter} from "@voltprotocol/rate-limits/IGlobalRateLimitedMinter.sol"; -import {IGlobalSystemExitRateLimiter} from "@voltprotocol/rate-limits/IGlobalSystemExitRateLimiter.sol"; import {IGlobalReentrancyLock, GlobalReentrancyLock} from "@voltprotocol/core/GlobalReentrancyLock.sol"; /// @title Source of truth for VOLT Protocol @@ -24,9 +23,6 @@ contract CoreV2 is ICoreV2, PermissionsV2 { /// @notice address of the global rate limited minter IGlobalRateLimitedMinter public globalRateLimitedMinter; - /// @notice address of the global system exit rate limiter - IGlobalSystemExitRateLimiter public globalSystemExitRateLimiter; - /// @notice address of the pcv oracle IPCVOracle public pcvOracle; @@ -83,20 +79,6 @@ contract CoreV2 is ICoreV2, PermissionsV2 { emit PCVOracleUpdate(oldPCVOracle, address(newPCVOracle)); } - /// @notice governor only function to set the Global Rate Limited Minter - /// @param newGlobalSystemExitRateLimiter new volt global rate limited minter - function setGlobalSystemExitRateLimiter( - IGlobalSystemExitRateLimiter newGlobalSystemExitRateLimiter - ) external onlyGovernor { - address oldGserl = address(globalSystemExitRateLimiter); - globalSystemExitRateLimiter = newGlobalSystemExitRateLimiter; - - emit GlobalSystemExitRateLimiterUpdate( - oldGserl, - address(newGlobalSystemExitRateLimiter) - ); - } - /// @notice governor only function to set the Global Reentrancy Lock /// @param newGlobalReentrancyLock new global reentrancy lock function setGlobalReentrancyLock( diff --git a/src/core/ICoreV2.sol b/src/core/ICoreV2.sol index 025f01ac0..42054c05d 100644 --- a/src/core/ICoreV2.sol +++ b/src/core/ICoreV2.sol @@ -6,7 +6,6 @@ import {IVolt, IERC20} from "@voltprotocol/volt/IVolt.sol"; import {IPermissionsV2} from "@voltprotocol/core/IPermissionsV2.sol"; import {IGlobalReentrancyLock} from "@voltprotocol/core/IGlobalReentrancyLock.sol"; import {IGlobalRateLimitedMinter} from "@voltprotocol/rate-limits/IGlobalRateLimitedMinter.sol"; -import {IGlobalSystemExitRateLimiter} from "@voltprotocol/rate-limits/IGlobalSystemExitRateLimiter.sol"; /// @title Core Interface /// @author Volt Protocol @@ -31,12 +30,6 @@ interface ICoreV2 is IPermissionsV2 { address indexed newPcvOracle ); - /// @notice emitted when reference to global system exit rate limiter is updated - event GlobalSystemExitRateLimiterUpdate( - address indexed oldGserl, - address indexed newGserl - ); - /// @notice emitted when reference to global reentrancy lock is updated event GlobalReentrancyLockUpdate( address indexed oldGrl, @@ -80,12 +73,6 @@ interface ICoreV2 is IPermissionsV2 { IGlobalRateLimitedMinter newGlobalRateLimitedMinter ) external; - /// @notice governor only function to set the Global Rate Limited Minter - /// @param newGlobalSystemExitRateLimiter new volt global rate limited minter - function setGlobalSystemExitRateLimiter( - IGlobalSystemExitRateLimiter newGlobalSystemExitRateLimiter - ) external; - /// @notice governor only function to set the PCV Oracle /// @param newPCVOracle new volt pcv oracle function setPCVOracle(IPCVOracle newPCVOracle) external; diff --git a/src/core/IPermissionsV2.sol b/src/core/IPermissionsV2.sol index 4f7cb238c..71c566552 100644 --- a/src/core/IPermissionsV2.sol +++ b/src/core/IPermissionsV2.sol @@ -6,6 +6,7 @@ import {IAccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; /// @title Permissions interface /// @author Volt Protocol interface IPermissionsV2 is IAccessControl { + // ----------- Governor only state changing api ----------- function createRole(bytes32 role, bytes32 adminRole) external; @@ -22,17 +23,7 @@ interface IPermissionsV2 is IAccessControl { function grantPCVGuard(address pcvGuard) external; - function grantRateLimitedMinter(address rateLimitedMinter) external; - - function grantRateLimitedRedeemer(address rateLimitedRedeemer) external; - - function grantSystemExitRateLimitDepleter( - address rateLimitedDepleter - ) external; - - function grantSystemExitRateLimitReplenisher( - address rateLimitedReplenisher - ) external; + function grantPsmMinter(address rateLimitedMinter) external; function revokeMinter(address minter) external; @@ -46,17 +37,7 @@ interface IPermissionsV2 is IAccessControl { function revokePCVGuard(address pcvGuard) external; - function revokeRateLimitedMinter(address rateLimitedMinter) external; - - function revokeRateLimitedRedeemer(address rateLimitedRedeemer) external; - - function revokeSystemExitRateLimitDepleter( - address rateLimitedDepleter - ) external; - - function revokeSystemExitRateLimitReplenisher( - address rateLimitedReplenisher - ) external; + function revokePsmMinter(address rateLimitedMinter) external; // ----------- Revoker only state changing api ----------- @@ -76,19 +57,7 @@ interface IPermissionsV2 is IAccessControl { function isPCVGuard(address _address) external view returns (bool); - function isRateLimitedMinter(address _address) external view returns (bool); - - function isRateLimitedRedeemer( - address _address - ) external view returns (bool); - - function isSystemExitRateLimitReplenisher( - address _address - ) external view returns (bool); - - function isSystemExitRateLimitDepleter( - address _address - ) external view returns (bool); + function isPsmMinter(address _address) external view returns (bool); // ----------- Predefined Roles ----------- @@ -115,14 +84,7 @@ interface IPermissionsV2 is IAccessControl { /// @notice granted to peg stability modules that will call in to deplete buffer /// and mint Volt - function RATE_LIMIT_SYSTEM_ENTRY_DEPLETE_ROLE() - external - view - returns (bytes32); - - /// @notice granted to peg stability modules that will call in to replenish the - /// buffer Volt is minted from - function RATE_LIMIT_SYSTEM_ENTRY_REPLENISH_ROLE() + function PSM_MINTER() external view returns (bytes32); diff --git a/src/core/PermissionsV2.sol b/src/core/PermissionsV2.sol index 73c9c4833..fd496f541 100644 --- a/src/core/PermissionsV2.sol +++ b/src/core/PermissionsV2.sol @@ -26,23 +26,9 @@ contract PermissionsV2 is IPermissionsV2, AccessControlEnumerable { /// @notice granted to EOA's to enable movement of funds to safety in an emergency bytes32 public constant PCV_GUARD_ROLE = keccak256("PCV_GUARD_ROLE"); - /// @notice granted to peg stability modules that will call in to deplete buffer - /// and mint Volt - bytes32 public constant RATE_LIMIT_SYSTEM_ENTRY_DEPLETE_ROLE = - keccak256("RATE_LIMIT_SYSTEM_ENTRY_DEPLETE_ROLE"); - - /// @notice granted to peg stability modules that will call in to replenish the - /// buffer Volt is minted from - bytes32 public constant RATE_LIMIT_SYSTEM_ENTRY_REPLENISH_ROLE = - keccak256("RATE_LIMIT_SYSTEM_ENTRY_REPLENISH_ROLE"); - - /// @notice can replenish buffer through GlobalSystemExitRateLimiter - bytes32 public constant RATE_LIMIT_SYSTEM_EXIT_REPLENISH_ROLE = - keccak256("RATE_LIMIT_SYSTEM_EXIT_REPLENISH_ROLE"); - - /// @notice can delpete buffer through the GlobalSystemExitRateLimiter buffer - bytes32 public constant RATE_LIMIT_SYSTEM_EXIT_DEPLETE_ROLE = - keccak256("RATE_LIMIT_SYSTEM_EXIT_DEPLETE_ROLE"); + /// @notice can replenish and deplete buffer through the GlobalRateLimiter. + /// replenishing burns Volt, depleting mints Volt + bytes32 public constant PSM_MINTER = keccak256("PSM_MINTER_ROLE"); /// @notice granted to system smart contracts to enable the setting /// of reentrancy locks within the GlobalReentrancyLock contract @@ -58,10 +44,7 @@ contract PermissionsV2 is IPermissionsV2, AccessControlEnumerable { _setRoleAdmin(GUARDIAN_ROLE, GOVERNOR_ROLE); _setRoleAdmin(LOCKER_ROLE, GOVERNOR_ROLE); _setRoleAdmin(PCV_GUARD_ROLE, GOVERNOR_ROLE); - _setRoleAdmin(RATE_LIMIT_SYSTEM_ENTRY_DEPLETE_ROLE, GOVERNOR_ROLE); - _setRoleAdmin(RATE_LIMIT_SYSTEM_ENTRY_REPLENISH_ROLE, GOVERNOR_ROLE); - _setRoleAdmin(RATE_LIMIT_SYSTEM_EXIT_REPLENISH_ROLE, GOVERNOR_ROLE); - _setRoleAdmin(RATE_LIMIT_SYSTEM_EXIT_DEPLETE_ROLE, GOVERNOR_ROLE); + _setRoleAdmin(PSM_MINTER, GOVERNOR_ROLE); } /// @notice callable only by governor @@ -86,10 +69,11 @@ contract PermissionsV2 is IPermissionsV2, AccessControlEnumerable { /// @param role the new role id /// @param adminRole the admin role id for `role` /// @dev can also be used to update admin of existing role - function createRole( - bytes32 role, - bytes32 adminRole - ) external override onlyGovernor { + function createRole(bytes32 role, bytes32 adminRole) + external + override + onlyGovernor + { _setRoleAdmin(role, adminRole); } @@ -101,9 +85,11 @@ contract PermissionsV2 is IPermissionsV2, AccessControlEnumerable { /// @notice grants controller role to address /// @param pcvController new controller - function grantPCVController( - address pcvController - ) external override onlyGovernor { + function grantPCVController(address pcvController) + external + override + onlyGovernor + { _grantRole(PCV_CONTROLLER_ROLE, pcvController); } @@ -121,9 +107,11 @@ contract PermissionsV2 is IPermissionsV2, AccessControlEnumerable { /// @notice grants level one locker role to address /// @param levelOneLocker new level one locker address - function grantLocker( - address levelOneLocker - ) external override onlyGovernor { + function grantLocker(address levelOneLocker) + external + override + onlyGovernor + { _grantRole(LOCKER_ROLE, levelOneLocker); } @@ -135,39 +123,12 @@ contract PermissionsV2 is IPermissionsV2, AccessControlEnumerable { /// @notice grants ability to mint Volt through the global rate limited minter /// @param rateLimitedMinter address to add as a minter in global rate limited minter - function grantRateLimitedMinter( - address rateLimitedMinter - ) external override onlyGovernor { - _grantRole(RATE_LIMIT_SYSTEM_ENTRY_DEPLETE_ROLE, rateLimitedMinter); - } - - /// @notice grants ability to replenish buffer for minting Volt through the global rate limited minter - /// @param rateLimitedRedeemer address to add as a redeemer in global rate limited minter - function grantRateLimitedRedeemer( - address rateLimitedRedeemer - ) external override onlyGovernor { - _grantRole(RATE_LIMIT_SYSTEM_ENTRY_REPLENISH_ROLE, rateLimitedRedeemer); - } - - /// @notice grants ability to replenish buffer for funds exiting system through - /// the global system exit rate limiter - /// @param rateLimitedReplenisher address to add as a replenisher in global system exit rate limiter - function grantSystemExitRateLimitReplenisher( - address rateLimitedReplenisher - ) external override onlyGovernor { - _grantRole( - RATE_LIMIT_SYSTEM_EXIT_REPLENISH_ROLE, - rateLimitedReplenisher - ); - } - - /// @notice grants ability to deplete buffer for funds exiting system through - /// the global system exit rate limiter - /// @param rateLimitedDepleter address to add as a depleter in global system exit rate limiter - function grantSystemExitRateLimitDepleter( - address rateLimitedDepleter - ) external override onlyGovernor { - _grantRole(RATE_LIMIT_SYSTEM_EXIT_DEPLETE_ROLE, rateLimitedDepleter); + function grantPsmMinter(address rateLimitedMinter) + external + override + onlyGovernor + { + _grantRole(PSM_MINTER, rateLimitedMinter); } /// @notice revokes minter role from address @@ -178,9 +139,11 @@ contract PermissionsV2 is IPermissionsV2, AccessControlEnumerable { /// @notice revokes pcvController role from address /// @param pcvController ex pcvController - function revokePCVController( - address pcvController - ) external override onlyGovernor { + function revokePCVController(address pcvController) + external + override + onlyGovernor + { _revokeRole(PCV_CONTROLLER_ROLE, pcvController); } @@ -198,9 +161,11 @@ contract PermissionsV2 is IPermissionsV2, AccessControlEnumerable { /// @notice revokes global locker role from address /// @param levelOneLocker ex globalLocker - function revokeLocker( - address levelOneLocker - ) external override onlyGovernor { + function revokeLocker(address levelOneLocker) + external + override + onlyGovernor + { _revokeRole(LOCKER_ROLE, levelOneLocker); } @@ -212,48 +177,22 @@ contract PermissionsV2 is IPermissionsV2, AccessControlEnumerable { /// @notice revokes ability to mint Volt through the global rate limited minter /// @param rateLimitedMinter ex minter in global rate limited minter - function revokeRateLimitedMinter( - address rateLimitedMinter - ) external override onlyGovernor { - _revokeRole(RATE_LIMIT_SYSTEM_ENTRY_DEPLETE_ROLE, rateLimitedMinter); - } - - /// @notice revokes ability to replenish buffer for minting Volt through the global rate limited minter - /// @param rateLimitedRedeemer ex redeemer in global rate limited minter - function revokeRateLimitedRedeemer( - address rateLimitedRedeemer - ) external override onlyGovernor { - _revokeRole( - RATE_LIMIT_SYSTEM_ENTRY_REPLENISH_ROLE, - rateLimitedRedeemer - ); - } - - /// @notice revokes ability to replenish buffer for funds exiting system through - /// the global system exit rate limiter - /// @param rateLimitedRedeemer ex replenisher in global system exit rate limiter - function revokeSystemExitRateLimitReplenisher( - address rateLimitedRedeemer - ) external override onlyGovernor { - _revokeRole(RATE_LIMIT_SYSTEM_EXIT_REPLENISH_ROLE, rateLimitedRedeemer); - } - - /// @notice revokes ability to deplete buffer for funds exiting system through - /// the global system exit rate limiter - /// @param rateLimitedRedeemer ex depleter in global system exit rate limiter - function revokeSystemExitRateLimitDepleter( - address rateLimitedRedeemer - ) external override onlyGovernor { - _revokeRole(RATE_LIMIT_SYSTEM_EXIT_DEPLETE_ROLE, rateLimitedRedeemer); + function revokePsmMinter(address rateLimitedMinter) + external + override + onlyGovernor + { + _revokeRole(PSM_MINTER, rateLimitedMinter); } /// @notice revokes a role from address /// @param role the role to revoke /// @param account the address to revoke the role from - function revokeOverride( - bytes32 role, - address account - ) external override onlyGuardian { + function revokeOverride(bytes32 role, address account) + external + override + onlyGuardian + { require( role != GOVERNOR_ROLE, "Permissions: Guardian cannot revoke governor" @@ -267,9 +206,13 @@ contract PermissionsV2 is IPermissionsV2, AccessControlEnumerable { /// @param _address address to check /// @return true _address is a minter // only virtual for testing mock override - function isMinter( - address _address - ) external view virtual override returns (bool) { + function isMinter(address _address) + external + view + virtual + override + returns (bool) + { return hasRole(MINTER_ROLE, _address); } @@ -277,9 +220,13 @@ contract PermissionsV2 is IPermissionsV2, AccessControlEnumerable { /// @param _address address to check /// @return true _address is a controller // only virtual for testing mock override - function isPCVController( - address _address - ) external view virtual override returns (bool) { + function isPCVController(address _address) + external + view + virtual + override + returns (bool) + { return hasRole(PCV_CONTROLLER_ROLE, _address); } @@ -287,9 +234,13 @@ contract PermissionsV2 is IPermissionsV2, AccessControlEnumerable { /// @param _address address to check /// @return true _address is a governor // only virtual for testing mock override - function isGovernor( - address _address - ) public view virtual override returns (bool) { + function isGovernor(address _address) + public + view + virtual + override + returns (bool) + { return hasRole(GOVERNOR_ROLE, _address); } @@ -297,9 +248,13 @@ contract PermissionsV2 is IPermissionsV2, AccessControlEnumerable { /// @param _address address to check /// @return true _address is a guardian // only virtual for testing mock override - function isGuardian( - address _address - ) public view virtual override returns (bool) { + function isGuardian(address _address) + public + view + virtual + override + returns (bool) + { return hasRole(GUARDIAN_ROLE, _address); } @@ -313,45 +268,24 @@ contract PermissionsV2 is IPermissionsV2, AccessControlEnumerable { /// @notice checks if address has PCV Guard role /// @param _address address to check /// @return true if _address has PCV Guard role - function isPCVGuard( - address _address - ) external view override returns (bool) { + function isPCVGuard(address _address) + external + view + override + returns (bool) + { return hasRole(PCV_GUARD_ROLE, _address); } /// @notice checks if address has Volt Minter Role /// @param _address address to check /// @return true if _address has Volt Minter Role - function isRateLimitedMinter( - address _address - ) external view override returns (bool) { - return hasRole(RATE_LIMIT_SYSTEM_ENTRY_DEPLETE_ROLE, _address); - } - - /// @notice checks if address has Volt Redeemer Role - /// @param _address address to check - /// @return true if _address has Volt Redeemer Role - function isRateLimitedRedeemer( - address _address - ) external view override returns (bool) { - return hasRole(RATE_LIMIT_SYSTEM_ENTRY_REPLENISH_ROLE, _address); - } - - /// @notice checks if address has Volt Rate Limited Replenisher Role - /// @param _address address to check - /// @return true if _address has Volt Rate Limited Replenisher Role - function isSystemExitRateLimitReplenisher( - address _address - ) external view override returns (bool) { - return hasRole(RATE_LIMIT_SYSTEM_EXIT_REPLENISH_ROLE, _address); - } - - /// @notice checks if address has Volt Rate Limited Depleter Role - /// @param _address address to check - /// @return true if _address has Volt Rate Limited Depleter Role - function isSystemExitRateLimitDepleter( - address _address - ) external view override returns (bool) { - return hasRole(RATE_LIMIT_SYSTEM_EXIT_DEPLETE_ROLE, _address); + function isPsmMinter(address _address) + external + view + override + returns (bool) + { + return hasRole(PSM_MINTER, _address); } } diff --git a/src/core/VoltRoles.sol b/src/core/VoltRoles.sol index af2341381..1fd76b210 100644 --- a/src/core/VoltRoles.sol +++ b/src/core/VoltRoles.sol @@ -38,22 +38,10 @@ library VoltRoles { /// ----------- Rate limiters for Global System Entry / Exit --------------- - /// @notice can mint VOLT through GlobalRateLimitedMinter on a rate limit - bytes32 internal constant RATE_LIMIT_SYSTEM_ENTRY_DEPLETE = - keccak256("RATE_LIMIT_SYSTEM_ENTRY_DEPLETE_ROLE"); - - /// @notice can redeem VOLT and replenish the GlobalRateLimitedMinter buffer - /// @notice non custodial PSM role. - bytes32 internal constant RATE_LIMIT_SYSTEM_ENTRY_REPLENISH = - keccak256("RATE_LIMIT_SYSTEM_ENTRY_REPLENISH_ROLE"); - - /// @notice can delpete buffer through the GlobalSystemExitRateLimiter buffer - bytes32 internal constant RATE_LIMIT_SYSTEM_EXIT_DEPLETE = - keccak256("RATE_LIMIT_SYSTEM_EXIT_DEPLETE_ROLE"); - - /// @notice can replenish buffer through GlobalSystemExitRateLimiter - bytes32 internal constant RATE_LIMIT_SYSTEM_EXIT_REPLENISH = - keccak256("RATE_LIMIT_SYSTEM_EXIT_REPLENISH_ROLE"); + /// @notice can replenish and deplete buffer through the GlobalRateLimiter. + /// replenishing burns Volt, depleting mints Volt + bytes32 internal constant PSM_MINTER = + keccak256("PSM_MINTER_ROLE"); /*/////////////////////////////////////////////////////////////// Minor Roles diff --git a/src/pcv/ERC20Allocator.sol b/src/pcv/ERC20Allocator.sol deleted file mode 100644 index dfcf8b1b0..000000000 --- a/src/pcv/ERC20Allocator.sol +++ /dev/null @@ -1,411 +0,0 @@ -pragma solidity =0.8.13; - -import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {Address} from "@openzeppelin/contracts/utils/Address.sol"; -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; -import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; - -import {CoreRefV2} from "@voltprotocol/refs/CoreRefV2.sol"; -import {IPCVDepositV2} from "@voltprotocol/pcv/IPCVDepositV2.sol"; -import {IERC20Allocator} from "@voltprotocol/pcv/IERC20Allocator.sol"; - -/// @notice Contract to remove all excess funds past a target balance from a smart contract -/// and to add funds to that same smart contract when it is under the target balance. -/// First application is allocating funds from a PSM to a yield venue so that liquid reserves are minimized. -/// This contract should never hold PCV, however it has a sweep function, so if tokens get sent to it accidentally, -/// they can still be recovered. - -/// This contract stores each PSM and maps it to the target balance and decimals normalizer for that token -/// PCV Deposits can then be linked to these PSM's which allows funds to be pushed and pulled -/// between these PCV deposits and their respective PSM's. -/// This design allows multiple PCV Deposits to be linked to a single PSM. - -/// This contract enforces the assumption that all pcv deposits connected to a given PSM share -/// the same underlying token, otherwise the rate limited logic will not work as intended. -/// This assumption is encoded in the create and edit deposit functions as well as the -/// connect deposit function. - -/// @author Elliot Friedman -contract ERC20Allocator is IERC20Allocator, CoreRefV2 { - using Address for address payable; - using SafeERC20 for IERC20; - using SafeCast for *; - - /// @notice container that stores information on all psm's - struct PSMInfo { - /// @notice target token address to send - address token; - /// @notice only skim if balance of target is greater than targetBalance - /// only drip if balance of target is less than targetBalance - uint248 targetBalance; - /// @notice decimal normalizer to ensure buffer is updated uniformly across all deposits - int8 decimalsNormalizer; - } - - /// @notice map the psm address to the corresponding target balance information - /// excess tokens past target balance will be pulled from the PSM - /// if PSM has less than the target balance, tokens will be sent to the PSM - mapping(address => PSMInfo) public allPSMs; - - /// @notice map the pcv deposit address to a peg stability module - mapping(address => address) public pcvDepositToPSM; - - /// @notice ERC20 Allocator constructor - /// @param _core Volt Core for reference - constructor(address _core) CoreRefV2(_core) {} - - /// ----------- Governor Only API ----------- - - /// @notice connect a new PSM - /// @param psm Peg Stability Module to add - /// @param psmTargetBalance target amount of tokens for the PSM to hold - /// @param decimalsNormalizer decimal normalizer to ensure buffer is depleted and replenished properly - function connectPSM( - address psm, - uint248 psmTargetBalance, - int8 decimalsNormalizer - ) external override onlyGovernor { - address token = IPCVDepositV2(psm).token(); - - require( - allPSMs[psm].token == address(0), - "ERC20Allocator: cannot overwrite existing deposit" - ); - - PSMInfo memory newPSM = PSMInfo({ - token: token, - targetBalance: psmTargetBalance, - decimalsNormalizer: decimalsNormalizer - }); - allPSMs[psm] = newPSM; - - emit PSMConnected(psm, token, psmTargetBalance, decimalsNormalizer); - } - - /// @notice edit an existing PSM - /// @param psm Peg Stability Module for this deposit - /// @param psmTargetBalance target amount of tokens for the PSM to hold - /// cannot manually change the underlying token, as this is pulled from the PSM - /// underlying token is immutable in both pcv deposit and - function editPSMTargetBalance( - address psm, - uint248 psmTargetBalance - ) external override onlyGovernor { - address token = IPCVDepositV2(psm).token(); - address storedToken = allPSMs[psm].token; - require( - storedToken != address(0), - "ERC20Allocator: cannot edit non-existent deposit" - ); - require(token == storedToken, "ERC20Allocator: psm changed underlying"); - - PSMInfo storage psmToEdit = allPSMs[psm]; - psmToEdit.targetBalance = psmTargetBalance; - - emit PSMTargetBalanceUpdated(psm, psmTargetBalance); - } - - /// @notice disconnect an existing deposit from the allocator - /// @param psm Peg Stability Module to remove from allocation - function disconnectPSM(address psm) external override onlyGovernor { - delete allPSMs[psm]; - - emit PSMDeleted(psm); - } - - /// @notice function to connect deposit to a PSM - /// this then allows the pulling of funds between the deposit and the PSM permissionlessly - /// as defined by the target balance set in allPSM's - /// this function does not check if the pcvDepositToPSM has already been connected - /// as only the governor can call and create, and overwriting with the same data (no op) is fine - /// @param psm peg stability module - /// @param pcvDeposit deposit to connect to psm - function connectDeposit( - address psm, - address pcvDeposit - ) external override onlyGovernor { - address pcvToken = allPSMs[psm].token; - - /// assert pcv deposit and psm share same denomination - require( - IPCVDepositV2(pcvDeposit).token() == pcvToken, - "ERC20Allocator: token mismatch" - ); - require(pcvToken != address(0), "ERC20Allocator: invalid underlying"); - - pcvDepositToPSM[pcvDeposit] = psm; - - emit DepositConnected(psm, pcvDeposit); - } - - /// @notice delete an existing deposit, callable only by governor - /// @param pcvDeposit PCV Deposit to remove connection to PSM - function deleteDeposit(address pcvDeposit) external override onlyGovernor { - delete pcvDepositToPSM[pcvDeposit]; - - emit DepositDeleted(pcvDeposit); - } - - /// ----------- Permissionless PCV Allocation APIs ----------- - - /// @notice pull ERC20 tokens from PSM and send to PCV Deposit - /// if the amount of tokens held in the PSM is above - /// the target balance. - /// @param pcvDeposit deposit to send excess funds to - function skim(address pcvDeposit) external whenNotPaused globalLock(1) { - address psm = pcvDepositToPSM[pcvDeposit]; - require(psm != address(0), "ERC20Allocator: invalid PCVDeposit"); - - _skim(psm, pcvDeposit); - } - - /// helper function that does the skimming - /// @param psm peg stability module to skim funds from - /// @param pcvDeposit pcv deposit to send funds to - function _skim(address psm, address pcvDeposit) internal { - /// Check - - /// note this check is redundant, as calculating amountToPull will revert - /// if pullThreshold is greater than the current balance of psm - /// however, we like to err on the side of verbosity - require( - _checkSkimCondition(psm), - "ERC20Allocator: skim condition not met" - ); - - (uint256 amountToSkim, uint256 adjustedAmountToSkim) = getSkimDetails( - pcvDeposit - ); - - /// Effects - - globalSystemExitRateLimiter().replenishBuffer(adjustedAmountToSkim); /// Effect -- trusted contract - - /// Interactions - - /// pull funds from pull target and send to push target - /// automatically pulls underlying token - IPCVDepositV2(psm).withdraw(pcvDeposit, amountToSkim); - - /// deposit pulled funds into the selected yield venue - IPCVDepositV2(pcvDeposit).deposit(); - - emit Skimmed(amountToSkim, pcvDeposit); - } - - /// @notice push ERC20 tokens to PSM by pulling from a PCV deposit - /// flow of funds: PCV Deposit -> PSM - /// @param pcvDeposit to pull funds from and send to corresponding PSM - function drip(address pcvDeposit) external whenNotPaused globalLock(1) { - address psm = pcvDepositToPSM[pcvDeposit]; - require(psm != address(0), "ERC20Allocator: invalid PCVDeposit"); - - _drip(psm, IPCVDepositV2(pcvDeposit)); - } - - /// helper function that does the dripping - /// @param psm peg stability module to drip to - /// @param pcvDeposit pcv deposit to pull funds from - function _drip(address psm, IPCVDepositV2 pcvDeposit) internal { - /// Check - require( - _checkDripCondition(psm, pcvDeposit), - "ERC20Allocator: drip condition not met" - ); - - (uint256 amountToDrip, uint256 adjustedAmountToDrip) = getDripDetails( - psm, - address(pcvDeposit) - ); - - /// Effects - - /// deplete buffer with adjusted amount so that it gets - /// depleted uniformly across all assets and deposits - globalSystemExitRateLimiter().depleteBuffer(adjustedAmountToDrip); /// Effect -- trusted contract - - /// Interaction - - /// drip amount to pcvDeposit psm so that it has targetBalance amount of tokens - pcvDeposit.withdraw(psm, amountToDrip); - emit Dripped(amountToDrip, psm); - } - - /// @notice does an action if any are available - /// @param pcvDeposit whose corresponding peg stability module action will be run on - function doAction(address pcvDeposit) external whenNotPaused globalLock(1) { - address psm = pcvDepositToPSM[pcvDeposit]; - require(psm != address(0), "ERC20Allocator: invalid PCVDeposit"); - - /// don't check buffer != 0 as that will happen in drip function on effects - if (_checkDripCondition(psm, IPCVDepositV2(pcvDeposit))) { - _drip(psm, IPCVDepositV2(pcvDeposit)); - } else if (_checkSkimCondition(psm)) { - _skim(psm, pcvDeposit); - } - } - - /// ----------- PURE & VIEW Only APIs ----------- - - /// @notice returns the target balance for a given PSM - function targetBalance(address psm) external view returns (uint256) { - return allPSMs[psm].targetBalance; - } - - /// @notice function to get the adjusted amount out - /// @param amountToDrip the amount to adjust - /// @param decimalsNormalizer the amount of decimals to adjust amount by - function getAdjustedAmount( - uint256 amountToDrip, - int8 decimalsNormalizer - ) public pure returns (uint256 adjustedAmountToDrip) { - if (decimalsNormalizer == 0) { - adjustedAmountToDrip = amountToDrip; - } else if (decimalsNormalizer > 0) { - uint256 scalingFactor = 10 ** decimalsNormalizer.toUint256(); - adjustedAmountToDrip = amountToDrip * scalingFactor; - } else { - uint256 scalingFactor = 10 ** (-1 * decimalsNormalizer).toUint256(); - adjustedAmountToDrip = amountToDrip / scalingFactor; - } - } - - /// @notice return the amount that can be skimmed off a given PSM - /// @param pcvDeposit pcv deposit whose corresponding psm will have skim amount checked - /// returns amount that can be skimmed, adjusted amount to skim and target to send proceeds - /// reverts if not skim eligbile - function getSkimDetails( - address pcvDeposit - ) public view returns (uint256 amountToSkim, uint256 adjustedAmountToSkim) { - address psm = pcvDepositToPSM[pcvDeposit]; - PSMInfo memory toSkim = allPSMs[psm]; - - address token = toSkim.token; - /// underflows when not skim eligble and reverts - amountToSkim = IERC20(token).balanceOf(psm) - toSkim.targetBalance; - - /// adjust amount to skim based on the decimals normalizer to replenish buffer - adjustedAmountToSkim = getAdjustedAmount( - amountToSkim, - toSkim.decimalsNormalizer - ); - } - - /// @notice return the amount that can be dripped to a given PSM - /// @param psm peg stability module to check drip amount on - /// @param pcvDeposit pcv deposit to drip from - /// returns amount that can be dripped, adjusted amount to drip and target - /// reverts if not drip eligbile - function getDripDetails( - address psm, - address pcvDeposit - ) public view returns (uint256 amountToDrip, uint256 adjustedAmountToDrip) { - PSMInfo memory toDrip = allPSMs[psm]; - - /// direct balanceOf call is cheaper than calling balance on psm - /// underflows when not drip eligble and reverts - uint256 targetBalanceDelta = toDrip.targetBalance - - IERC20(toDrip.token).balanceOf(psm); - - /// drip min between target drip amount and pcv deposit being pulled from - /// to prevent edge cases when a venue runs out of liquidity - /// only drip the lowest between amount and the buffer, - /// as dripping more than the buffer will result in a revert in the _drip function - - /// example: usdc deposits - /// decimal normalizer = 12 - /// target balance delta = 10,000e6 - /// pcvDeposit.balance = 5,000e6 - /// buffer = 1,000e18 - - /// getAdjustedAmount(1,000e18, -12) = 1,000e6 - - /// amountToDrip = 1,000e6 - - amountToDrip = Math.min( - Math.min(targetBalanceDelta, IPCVDepositV2(pcvDeposit).balance()), - /// adjust for decimals here as buffer is 1e18 scaled, - /// and if token is not scaled by 1e18, then this amountToDrip could be over the buffer - /// because buffer is 1e18 adjusted, and decimals normalizer is used to adjust up to the buffer - /// need to invert decimals normalizer for this to work properly - getAdjustedAmount( - globalSystemExitRateLimiter().buffer(), - toDrip.decimalsNormalizer * -1 - ) - ); - - /// adjust amount to drip based on the decimals normalizer to deplete buffer - adjustedAmountToDrip = getAdjustedAmount( - amountToDrip, - toDrip.decimalsNormalizer - ); - } - - /// @notice function that returns whether the amount of tokens held - /// are below the target and funds should flow from PCV Deposit -> PSM - /// returns false when paused - /// @param pcvDeposit pcv deposit whose corresponding peg stability module to check drip condition - function checkDripCondition( - address pcvDeposit - ) external view override returns (bool) { - /// if paused or buffer empty, cannot drip - if (paused() == true || globalSystemExitRateLimiter().buffer() == 0) { - return false; - } - - address psm = pcvDepositToPSM[pcvDeposit]; - return _checkDripCondition(psm, IPCVDepositV2(pcvDeposit)); - } - - /// @notice function that returns whether the amount of tokens held - /// are above the target and funds should flow from PSM -> PCV Deposit - /// returns false when paused - function checkSkimCondition( - address pcvDeposit - ) external view override returns (bool) { - if (paused() == true) { - return false; - } - - address psm = pcvDepositToPSM[pcvDeposit]; - return _checkSkimCondition(psm); - } - - /// @notice returns whether an action is allowed - /// returns false when paused - function checkActionAllowed( - address pcvDeposit - ) external view override returns (bool) { - /// if paused, no actions allowed - if (paused() == true) { - return false; - } - - address psm = pcvDepositToPSM[pcvDeposit]; - /// cannot drip with an empty buffer - return - (globalSystemExitRateLimiter().buffer() != 0 && - _checkDripCondition(psm, IPCVDepositV2(pcvDeposit))) || - _checkSkimCondition(psm); - } - - function _checkDripCondition( - address psm, - IPCVDepositV2 pcvDeposit - ) internal view returns (bool) { - /// direct balanceOf call is cheaper than calling balance on psm - /// also cannot drip if balance in underlying venue is 0 - return - IERC20(allPSMs[psm].token).balanceOf(psm) < - allPSMs[psm].targetBalance && - pcvDeposit.balance() != 0; - } - - function _checkSkimCondition(address psm) internal view returns (bool) { - /// direct balanceOf call is cheaper than calling balance on psm - return - IERC20(allPSMs[psm].token).balanceOf(psm) > - allPSMs[psm].targetBalance; - } -} diff --git a/src/peg/BasePegStabilityModule.sol b/src/peg/BasePegStabilityModule.sol deleted file mode 100644 index 6fd6beb7c..000000000 --- a/src/peg/BasePegStabilityModule.sol +++ /dev/null @@ -1,195 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.13; - -import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; - -import {Constants} from "@voltprotocol/Constants.sol"; -import {OracleRefV2} from "@voltprotocol/refs/OracleRefV2.sol"; -import {PCVDeposit} from "@voltprotocol/pcv/PCVDeposit.sol"; -import {IPCVDeposit} from "@voltprotocol/pcv/IPCVDeposit.sol"; -import {IPegStabilityModule} from "@voltprotocol/peg/IPegStabilityModule.sol"; - -abstract contract BasePegStabilityModule is IPegStabilityModule, OracleRefV2 { - using SafeERC20 for IERC20; - using SafeCast for *; - - /// @notice the token this PSM will exchange for VOLT - IERC20 public immutable override underlyingToken; - - /// @notice the minimum acceptable oracle price floor - uint128 public override floor; - - /// @notice the maximum acceptable oracle price ceiling - uint128 public override ceiling; - - /// @notice construct the PSM - /// @param coreAddress reference to core - /// @param oracleAddress reference to oracle - /// @param backupOracle reference to backup oracle - /// @param decimalsNormalizer decimal normalizer for oracle price - /// @param doInvert invert oracle price - /// @param underlyingTokenAddress this psm uses - /// @param floorPrice minimum acceptable oracle price - /// @param ceilingPrice maximum acceptable oracle price - constructor( - address coreAddress, - address oracleAddress, - address backupOracle, - int256 decimalsNormalizer, - bool doInvert, - IERC20 underlyingTokenAddress, - uint128 floorPrice, - uint128 ceilingPrice - ) - OracleRefV2( - coreAddress, - oracleAddress, - backupOracle, - decimalsNormalizer, - doInvert - ) - { - _setCeiling(ceilingPrice); - _setFloor(floorPrice); - underlyingToken = underlyingTokenAddress; - } - - // ----------- Governor Only State Changing API ----------- - - /// @notice sets the new floor price - /// @param newFloorPrice new floor price - function setOracleFloorPrice( - uint128 newFloorPrice - ) external override onlyGovernor { - _setFloor(newFloorPrice); - } - - /// @notice sets the new ceiling price - /// @param newCeilingPrice new ceiling price - function setOracleCeilingPrice( - uint128 newCeilingPrice - ) external override onlyGovernor { - _setCeiling(newCeilingPrice); - } - - /// ----------- Public View-Only API ---------- - - /// @notice return address of token - function token() public view returns (address) { - return address(underlyingToken); - } - - /// @notice calculate the amount of VOLT out for a given `amountIn` of underlying - /// First get oracle price of token - /// Then figure out how many dollars that amount in is worth by multiplying price * amount. - /// ensure decimals are normalized if on underlying they are not 18 - /// @param amountIn amount of underlying token in - /// @return amountVoltOut the amount of Volt out - /// @dev reverts if price is out of allowed range - function getMintAmountOut( - uint256 amountIn - ) public view virtual returns (uint256 amountVoltOut) { - uint256 oraclePrice = readOracle(); - _validatePriceRange(oraclePrice); - - /// This was included to make sure that precision is retained when dividing - /// In the case where 1 USDC is deposited, which is 1e6, at the time of writing - /// the VOLT price is $1.05 so the price we retrieve from the oracle will be 1.05e6 - /// VOLT contains 18 decimals, so when we perform the below calculation, it amounts to - /// 1e6 * 1e18 / 1.05e6 = 1e24 / 1.05e6 which lands us at around 0.95e17, which is 0.95 - /// VOLT for 1 USDC which is consistent with the exchange rate - /// need to multiply by 1e18 before dividing because oracle price is scaled down by - /// -12 decimals in the case of USDC - - /// DAI example: - /// amountIn = 1e18 (1 DAI) - /// oraclePrice = 1.05e18 ($1.05/Volt) - /// amountVoltOut = (amountIn * 1e18) / oraclePrice - /// = 9.523809524E17 Volt out - amountVoltOut = (amountIn * 1e18) / oraclePrice; - } - - /// @notice calculate the amount of underlying out for a given `amountVoltIn` of Volt - /// First get oracle price of token - /// Then figure out how many dollars that amount in is worth by multiplying price * amount. - /// ensure decimals are normalized if on underlying they are not 18 - /// @dev reverts if price is out of allowed range - function getRedeemAmountOut( - uint256 amountVoltIn - ) public view override returns (uint256 amountTokenOut) { - uint256 oraclePrice = readOracle(); - _validatePriceRange(oraclePrice); - - /// DAI Example: - /// decimals normalizer: 0 - /// amountVoltIn = 1e18 (1 VOLT) - /// oraclePrice = 1.05e18 ($1.05/Volt) - /// amountTokenOut = oraclePrice * amountVoltIn / 1e18 - /// = 1.05e18 DAI out - - /// USDC Example: - /// decimals normalizer: -12 - /// amountVoltIn = 1e18 (1 VOLT) - /// oraclePrice = 1.05e6 ($1.05/Volt) - /// amountTokenOut = oraclePrice * amountVoltIn / 1e18 - /// = 1.05e6 USDC out - amountTokenOut = (oraclePrice * amountVoltIn) / 1e18; - } - - /// @notice function from PCVDeposit that must be overriden - function balance() public view returns (uint256) { - return underlyingToken.balanceOf(address(this)); - } - - /// @notice returns address of token this contracts balance is reported in - function balanceReportedIn() public view returns (address) { - return address(underlyingToken); - } - - /// @notice returns whether or not the current price is valid - function isPriceValid() external view override returns (bool) { - return _validPrice(readOracle()); - } - - /// ----------- Private Helper Functions ----------- - - /// @notice helper function to set the ceiling in basis points - function _setCeiling(uint128 newCeilingPrice) private { - require( - newCeilingPrice > floor, - "PegStabilityModule: ceiling must be greater than floor" - ); - uint128 oldCeiling = ceiling; - ceiling = newCeilingPrice; - - emit OracleCeilingUpdate(oldCeiling, newCeilingPrice); - } - - /// @notice helper function to set the floor in basis points - function _setFloor(uint128 newFloorPrice) private { - require(newFloorPrice != 0, "PegStabilityModule: invalid floor"); - require( - newFloorPrice < ceiling, - "PegStabilityModule: floor must be less than ceiling" - ); - uint128 oldFloor = floor; - floor = newFloorPrice; - - emit OracleFloorUpdate(oldFloor, newFloorPrice); - } - - /// @notice helper function to determine if price is within a valid range - /// @param price oracle price expressed as a decimal - function _validPrice(uint256 price) private view returns (bool valid) { - valid = price >= floor && price <= ceiling; - } - - /// @notice reverts if the price is greater than or equal to the ceiling or less than or equal to the floor - /// @param price oracle price expressed as a decimal - function _validatePriceRange(uint256 price) private view { - require(_validPrice(price), "PegStabilityModule: price out of bounds"); - } -} diff --git a/src/peg/INonCustodialPSM.sol b/src/peg/INonCustodialPSM.sol index a1a96904e..16d93f834 100644 --- a/src/peg/INonCustodialPSM.sol +++ b/src/peg/INonCustodialPSM.sol @@ -19,13 +19,80 @@ import {IPCVDepositV2} from "@voltprotocol/pcv/IPCVDepositV2.sol"; * Inspired by Tribe DAO and MakerDAO PSM */ interface INonCustodialPSM { - // ----------- Governor or Admin Only State Changing API ----------- + // ----------- Public State Changing API ----------- - /// @notice set the target for sending surplus reserves + /// @notice function to buy VOLT for an underlying asset + /// This contract has no minting functionality, so the max + /// amount of Volt that can be purchased is the Volt balance in the contract + /// @dev does not require non-reentrant modifier because this contract + /// stores no state. Even if USDC, DAI or any other token this contract uses + /// had an after transfer hook, calling mint or redeem in a reentrant fashion + /// would not allow any theft of funds, it would simply build up a call stack + /// of orders that would need to be executed. + /// @param to recipient of the Volt + /// @param amountIn amount of underlying tokens used to purchase Volt + /// @param minAmountOut minimum amount of Volt recipient to receive + function mint( + address to, + uint256 amountIn, + uint256 minAmountOut + ) external returns (uint256 amountVoltOut); + + /// @notice function to redeem VOLT for an underlying asset + /// @dev does not require non-reentrant modifier because this contract + /// stores no state. Even if USDC, DAI or any other token this contract uses + /// had an after transfer hook, calling mint or redeem in a reentrant fashion + /// would not allow any theft of funds, it would simply build up a call stack + /// of orders that would need to be executed. + /// @param to recipient of underlying tokens + /// @param amountVoltIn amount of volt to sell + /// @param minAmountOut of underlying tokens sent to recipient + function redeem( + address to, + uint256 amountVoltIn, + uint256 minAmountOut + ) external returns (uint256 amountOut); + + // ----------- Governor or admin only state changing api ----------- + + /// @notice sets the floor price in BP + function setOracleFloorPrice(uint128 newFloor) external; + + /// @notice sets the ceiling price in BP + function setOracleCeilingPrice(uint128 newCeiling) external; + + /// @notice set the target for sending proceeds and function setPCVDeposit(IPCVDepositV2 newTarget) external; // ----------- Getters ----------- + /// @notice get the floor price in basis points + function floor() external view returns (uint128); + + /// @notice get the ceiling price in basis points + function ceiling() external view returns (uint128); + + /// @notice return wether the current oracle price is valid or not + function isPriceValid() external view returns (bool); + + /// @notice calculate the amount of Volt out for a given `amountIn` of underlying + function getMintAmountOut( + uint256 amountIn + ) external view returns (uint256 amountVoltOut); + + /// @notice calculate the amount of underlying out for a given `amountVoltIn` of Volt + function getRedeemAmountOut( + uint256 amountVoltIn + ) external view returns (uint256 amountOut); + + /// @notice the underlying token exchanged for Volt + function underlyingToken() external view returns (IERC20); + + /// @notice returns the maximum amount of Volt that can be redeemed + /// Custodial PSM checks with the current PSM balance + /// Non Custodial PSM checks with the current PCV Deposit balance and the Global System Exit Rate Limit Buffer + function getMaxRedeemAmountIn() external view returns (uint256); + /// @notice the PCV deposit target to deposit and withdraw from function pcvDeposit() external view returns (IPCVDepositV2); @@ -33,4 +100,16 @@ interface INonCustodialPSM { /// @notice event emitted when surplus target is updated event PCVDepositUpdate(address oldTarget, address newTarget); + + /// @notice event emitted upon a redemption + event Redeem(address to, uint256 amountVoltIn, uint256 amountAssetOut); + + /// @notice event emitted when Volt gets minted + event Mint(address to, uint256 amountIn, uint256 amountVoltOut); + + /// @notice event emitted when minimum floor price is updated + event OracleFloorUpdate(uint128 oldFloor, uint128 newFloor); + + /// @notice event emitted when maximum ceiling price is updated + event OracleCeilingUpdate(uint128 oldCeiling, uint128 newCeiling); } diff --git a/src/peg/IPegStabilityModule.sol b/src/peg/IPegStabilityModule.sol index e96308bd6..ec4948508 100644 --- a/src/peg/IPegStabilityModule.sol +++ b/src/peg/IPegStabilityModule.sol @@ -93,41 +93,12 @@ interface IPegStabilityModule { // ----------- Events ----------- - event Withdrawal( - address indexed _caller, - address indexed _to, - uint256 _amount - ); - - /// @notice event emitted when erc20 tokens are withdrawn - event WithdrawERC20( - address indexed _caller, - address indexed _token, - address indexed _to, - uint256 _amount - ); - - /// @notice event emitted when excess PCV is allocated - event AllocateSurplus(address indexed caller, uint256 amount); - /// @notice event emitted upon a redemption event Redeem(address to, uint256 amountVoltIn, uint256 amountAssetOut); /// @notice event emitted when Volt gets minted event Mint(address to, uint256 amountIn, uint256 amountVoltOut); - /// @notice event that is emitted when redemptions are paused - event RedemptionsPaused(address account); - - /// @notice event that is emitted when redemptions are unpaused - event RedemptionsUnpaused(address account); - - /// @notice event that is emitted when minting is paused - event MintingPaused(address account); - - /// @notice event that is emitted when minting is unpaused - event MintingUnpaused(address account); - /// @notice event emitted when minimum floor price is updated event OracleFloorUpdate(uint128 oldFloor, uint128 newFloor); diff --git a/src/peg/NonCustodialPSM.sol b/src/peg/NonCustodialPSM.sol index aa4544ee0..ff4e3dba8 100644 --- a/src/peg/NonCustodialPSM.sol +++ b/src/peg/NonCustodialPSM.sol @@ -8,9 +8,9 @@ import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {Constants} from "@voltprotocol/Constants.sol"; import {OracleRefV2} from "@voltprotocol/refs/OracleRefV2.sol"; +import {CoreRefV2} from "@voltprotocol/refs/CoreRefV2.sol"; import {IPCVDepositV2} from "@voltprotocol/pcv/IPCVDepositV2.sol"; import {INonCustodialPSM} from "@voltprotocol/peg/INonCustodialPSM.sol"; -import {BasePegStabilityModule} from "@voltprotocol/peg/BasePegStabilityModule.sol"; /// @notice this contract needs the PCV controller role to be able to pull funds /// from the PCV deposit smart contract. @@ -20,10 +20,19 @@ import {BasePegStabilityModule} from "@voltprotocol/peg/BasePegStabilityModule.s /// in order to deplete the buffer in the GlobalSystemExitRateLimiter. /// This PSM is not a PCV deposit because it never holds funds, it only has permissions /// to pull funds from a pcv deposit and replenish a global buffer. -contract NonCustodialPSM is BasePegStabilityModule, INonCustodialPSM { +contract NonCustodialPSM is INonCustodialPSM, OracleRefV2 { using SafeERC20 for IERC20; using SafeCast for *; + /// @notice the token this PSM will exchange for VOLT + IERC20 public immutable override underlyingToken; + + /// @notice the minimum acceptable oracle price floor + uint128 public override floor; + + /// @notice the maximum acceptable oracle price ceiling + uint128 public override ceiling; + /// @notice reference to the fully liquid venue redemptions can occur in IPCVDepositV2 public pcvDeposit; @@ -47,17 +56,17 @@ contract NonCustodialPSM is BasePegStabilityModule, INonCustodialPSM { uint128 ceilingPrice, IPCVDepositV2 pcvDepositAddress ) - BasePegStabilityModule( + OracleRefV2( coreAddress, oracleAddress, backupOracleAddress, decimalNormalizer, - invert, - underlyingTokenAddress, - floorPrice, - ceilingPrice - ) - { + invert + ) { + underlyingToken = underlyingTokenAddress; + + _setFloor(floorPrice); + _setCeiling(ceilingPrice); _setPCVDeposit(pcvDepositAddress); } @@ -72,6 +81,22 @@ contract NonCustodialPSM is BasePegStabilityModule, INonCustodialPSM { _setPCVDeposit(newTarget); } + /// @notice sets the new floor price + /// @param newFloorPrice new floor price + function setOracleFloorPrice( + uint128 newFloorPrice + ) external override onlyGovernor { + _setFloor(newFloorPrice); + } + + /// @notice sets the new ceiling price + /// @param newCeilingPrice new ceiling price + function setOracleCeilingPrice( + uint128 newCeilingPrice + ) external override onlyGovernor { + _setCeiling(newCeilingPrice); + } + // ----------- Public State Changing API ----------- /// @notice function to redeem VOLT for an underlying asset @@ -107,7 +132,6 @@ contract NonCustodialPSM is BasePegStabilityModule, INonCustodialPSM { /// None of these three external calls make calls external to the Volt System. volt().burnFrom(msg.sender, amountVoltIn); /// Check and Effect -- trusted contract globalRateLimitedMinter().replenishBuffer(amountVoltIn); /// Effect -- trusted contract - globalSystemExitRateLimiter().depleteBuffer(getExitValue(amountOut)); /// Check and Effect -- trusted contract, reverts if buffer exhausted /// Interaction -- pcv deposit is trusted, /// however this interacts with external untrusted contracts to withdraw funds from a venue @@ -119,19 +143,50 @@ contract NonCustodialPSM is BasePegStabilityModule, INonCustodialPSM { emit Redeem(to, amountVoltIn, amountOut); } - /// @notice overriden and reverts to keep compatability with standard PSM interface - function mint(address, uint256, uint256) external pure returns (uint256) { - revert("NonCustodialPSM: cannot mint"); - } + /// @notice function to buy VOLT for an underlying asset + /// This contract has no minting functionality, so the max + /// amount of Volt that can be purchased is the Volt balance in the contract + /// @dev does not require non-reentrant modifier because this contract + /// stores no state. Even if USDC, DAI or any other token this contract uses + /// had an after transfer hook, calling mint or redeem in a reentrant fashion + /// would not allow any theft of funds, it would simply build up a call stack + /// of orders that would need to be executed. + /// @param to recipient of the Volt + /// @param amountIn amount of underlying tokens used to purchase Volt + /// @param minAmountVoltOut minimum amount of Volt recipient to receive + function mint( + address to, + uint256 amountIn, + uint256 minAmountVoltOut + ) external override globalLock(1) returns (uint256 amountVoltOut) { + /// ------- Checks ------- + /// 1. current price from oracle is correct + /// 2. how much volt to receive + /// 3. volt to receive meets min amount out - /// ----------- Public View-Only API ---------- + amountVoltOut = getMintAmountOut(amountIn); + require( + amountVoltOut >= minAmountVoltOut, + "PegStabilityModule: Mint not enough out" + ); - /// @notice overriden and reverts to keep compatability with standard PSM interface - function getMintAmountOut(uint256) public view override returns (uint256) { - floor; /// shhh - revert("NonCustodialPSM: cannot mint"); + /// ------- Check / Effect / Trusted Interaction ------- + + /// Checks that there is enough Volt left to mint globally. + /// This is a check as well, because if there isn't sufficient Volt to mint, + /// then, the call to mintVolt will fail in the RateLimitedV2 class. + globalRateLimitedMinter().mintVolt(to, amountVoltOut); /// Check, Effect, then Interaction with trusted contract + + /// ------- Interactions with Untrusted Contract ------- + + underlyingToken.safeTransferFrom(msg.sender, address(pcvDeposit), amountIn); /// Interaction -- untrusted contract + pcvDeposit.deposit(); /// deposit into underlying venue to register new amount of PCV + + emit Mint(to, amountIn, amountVoltOut); } + /// ----------- Public View-Only API ---------- + /// @notice returns the maximum amount of Volt that can be redeemed /// with the current PCV Deposit balance and the Global System Exit Rate Limit Buffer function getMaxRedeemAmountIn() external view override returns (uint256) { @@ -154,7 +209,7 @@ contract NonCustodialPSM is BasePegStabilityModule, INonCustodialPSM { uint256 oraclePrice = readOracle(); /// amount of Volt that can exit the system through the exit rate limiter - uint256 bufferAllowableVoltAmountOut = (globalSystemExitRateLimiter() + uint256 bufferAllowableVoltAmountOut = (globalRateLimitedMinter() .buffer() * Constants.ETH_GRANULARITY) / oraclePrice; /// amount of Volt that can exit the system through the pcv deposit @@ -189,7 +244,76 @@ contract NonCustodialPSM is BasePegStabilityModule, INonCustodialPSM { } } - /// ----------- Private Helper Function ----------- + /// ----------- Public View-Only API ---------- + + /// @notice return address of token + function token() public view returns (address) { + return address(underlyingToken); + } + + /// @notice calculate the amount of VOLT out for a given `amountIn` of underlying + /// First get oracle price of token + /// Then figure out how many dollars that amount in is worth by multiplying price * amount. + /// ensure decimals are normalized if on underlying they are not 18 + /// @param amountIn amount of underlying token in + /// @return amountVoltOut the amount of Volt out + /// @dev reverts if price is out of allowed range + function getMintAmountOut( + uint256 amountIn + ) public view virtual returns (uint256 amountVoltOut) { + uint256 oraclePrice = readOracle(); + _validatePriceRange(oraclePrice); + + /// This was included to make sure that precision is retained when dividing + /// In the case where 1 USDC is deposited, which is 1e6, at the time of writing + /// the VOLT price is $1.05 so the price we retrieve from the oracle will be 1.05e6 + /// VOLT contains 18 decimals, so when we perform the below calculation, it amounts to + /// 1e6 * 1e18 / 1.05e6 = 1e24 / 1.05e6 which lands us at around 0.95e17, which is 0.95 + /// VOLT for 1 USDC which is consistent with the exchange rate + /// need to multiply by 1e18 before dividing because oracle price is scaled down by + /// -12 decimals in the case of USDC + + /// DAI example: + /// amountIn = 1e18 (1 DAI) + /// oraclePrice = 1.05e18 ($1.05/Volt) + /// amountVoltOut = (amountIn * 1e18) / oraclePrice + /// = 9.523809524E17 Volt out + amountVoltOut = (amountIn * 1e18) / oraclePrice; + } + + /// @notice calculate the amount of underlying out for a given `amountVoltIn` of Volt + /// First get oracle price of token + /// Then figure out how many dollars that amount in is worth by multiplying price * amount. + /// ensure decimals are normalized if on underlying they are not 18 + /// @dev reverts if price is out of allowed range + function getRedeemAmountOut( + uint256 amountVoltIn + ) public view override returns (uint256 amountTokenOut) { + uint256 oraclePrice = readOracle(); + _validatePriceRange(oraclePrice); + + /// DAI Example: + /// decimals normalizer: 0 + /// amountVoltIn = 1e18 (1 VOLT) + /// oraclePrice = 1.05e18 ($1.05/Volt) + /// amountTokenOut = oraclePrice * amountVoltIn / 1e18 + /// = 1.05e18 DAI out + + /// USDC Example: + /// decimals normalizer: -12 + /// amountVoltIn = 1e18 (1 VOLT) + /// oraclePrice = 1.05e6 ($1.05/Volt) + /// amountTokenOut = oraclePrice * amountVoltIn / 1e18 + /// = 1.05e6 USDC out + amountTokenOut = (oraclePrice * amountVoltIn) / 1e18; + } + + /// @notice returns whether or not the current price is valid + function isPriceValid() external view override returns (bool) { + return _validPrice(readOracle()); + } + + /// ----------- Private Helper Functions ----------- /// @notice helper function to set the PCV deposit /// @param newPCVDeposit the new PCV deposit that this PSM will pull assets from and deposit assets into @@ -203,4 +327,41 @@ contract NonCustodialPSM is BasePegStabilityModule, INonCustodialPSM { emit PCVDepositUpdate(address(oldTarget), address(newPCVDeposit)); } + + /// @notice helper function to set the ceiling in basis points + function _setCeiling(uint128 newCeilingPrice) private { + require( + newCeilingPrice > floor, + "PegStabilityModule: ceiling must be greater than floor" + ); + uint128 oldCeiling = ceiling; + ceiling = newCeilingPrice; + + emit OracleCeilingUpdate(oldCeiling, newCeilingPrice); + } + + /// @notice helper function to set the floor in basis points + function _setFloor(uint128 newFloorPrice) private { + require(newFloorPrice != 0, "PegStabilityModule: invalid floor"); + require( + newFloorPrice < ceiling, + "PegStabilityModule: floor must be less than ceiling" + ); + uint128 oldFloor = floor; + floor = newFloorPrice; + + emit OracleFloorUpdate(oldFloor, newFloorPrice); + } + + /// @notice helper function to determine if price is within a valid range + /// @param price oracle price expressed as a decimal + function _validPrice(uint256 price) private view returns (bool valid) { + valid = price >= floor && price <= ceiling; + } + + /// @notice reverts if the price is greater than or equal to the ceiling or less than or equal to the floor + /// @param price oracle price expressed as a decimal + function _validatePriceRange(uint256 price) private view { + require(_validPrice(price), "PegStabilityModule: price out of bounds"); + } } diff --git a/src/peg/PegStabilityModule.sol b/src/peg/PegStabilityModule.sol deleted file mode 100644 index b5c33e6ee..000000000 --- a/src/peg/PegStabilityModule.sol +++ /dev/null @@ -1,187 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.13; - -import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; - -import {Constants} from "@voltprotocol/Constants.sol"; -import {PCVDeposit} from "@voltprotocol/pcv/PCVDeposit.sol"; -import {IPCVDeposit} from "@voltprotocol/pcv/IPCVDeposit.sol"; -import {IPegStabilityModule} from "@voltprotocol/peg/IPegStabilityModule.sol"; -import {BasePegStabilityModule} from "@voltprotocol/peg/BasePegStabilityModule.sol"; - -contract PegStabilityModule is BasePegStabilityModule { - using SafeERC20 for IERC20; - using SafeCast for *; - - /// @notice construct the PSM - /// @param coreAddress reference to core - /// @param oracleAddress reference to oracle - /// @param backupOracle reference to backup oracle - /// @param decimalsNormalizer decimal normalizer for oracle price - /// @param doInvert invert oracle price - /// @param underlyingTokenAddress this psm uses - /// @param floorPrice minimum acceptable oracle price - /// @param ceilingPrice maximum acceptable oracle price - constructor( - address coreAddress, - address oracleAddress, - address backupOracle, - int256 decimalsNormalizer, - bool doInvert, - IERC20 underlyingTokenAddress, - uint128 floorPrice, - uint128 ceilingPrice - ) - BasePegStabilityModule( - coreAddress, - oracleAddress, - backupOracle, - decimalsNormalizer, - doInvert, - underlyingTokenAddress, - floorPrice, - ceilingPrice - ) - {} - - // ----------- PCV Controller Only State Changing API ----------- - - /// @notice withdraw assets from PSM to an external address - /// @param to recipient - /// @param amount of tokens to withdraw - function withdraw( - address to, - uint256 amount - ) external onlyPCVController globalLock(2) { - underlyingToken.safeTransfer(to, amount); - emit Withdrawal(msg.sender, to, amount); - } - - /// @notice withdraw ERC20 from the contract - /// @param token address of the ERC20 to send - /// @param to address destination of the ERC20 - /// @param amount quantity of ERC20 to send - function withdrawERC20( - address token, - address to, - uint256 amount - ) external onlyPCVController { - IERC20(token).safeTransfer(to, amount); - emit WithdrawERC20(msg.sender, token, to, amount); - } - - // ----------- Public State Changing API ----------- - - /// @notice function to redeem VOLT for an underlying asset - /// @dev does not require non-reentrant modifier because this contract - /// stores no state. Even if USDC, DAI or any other token this contract uses - /// had an after transfer hook, calling mint or redeem in a reentrant fashion - /// would not allow any theft of funds, it would simply build up a call stack - /// of orders that would need to be executed. - /// @param to recipient of underlying tokens - /// @param amountVoltIn amount of volt to sell - /// @param minAmountOut of underlying tokens sent to recipient - function redeem( - address to, - uint256 amountVoltIn, - uint256 minAmountOut - ) external override globalLock(1) returns (uint256 amountOut) { - /// ------- Checks ------- - /// 1. current price from oracle is correct - /// 2. how much underlying token to receive - /// 3. underlying token to receive meets min amount out - - amountOut = getRedeemAmountOut(amountVoltIn); - require( - amountOut >= minAmountOut, - "PegStabilityModule: Redeem not enough out" - ); - - /// ------- Effects / Interactions with Internal Contracts ------- - - /// Do effect after interaction because you don't want to give tokens before - /// taking the corresponding amount of Volt from the account. - /// Replenishing buffer allows more Volt to be minted. - volt().burnFrom(msg.sender, amountVoltIn); /// Check and Interaction with a trusted contract - globalRateLimitedMinter().replenishBuffer(amountVoltIn); /// Effect -- interaction with a trusted contract - - /// ------- Interaction with External Contract ------- - - underlyingToken.safeTransfer(to, amountOut); /// Interaction -- untrusted contract - - emit Redeem(to, amountVoltIn, amountOut); - } - - /// @notice function to buy VOLT for an underlying asset - /// This contract has no minting functionality, so the max - /// amount of Volt that can be purchased is the Volt balance in the contract - /// @dev does not require non-reentrant modifier because this contract - /// stores no state. Even if USDC, DAI or any other token this contract uses - /// had an after transfer hook, calling mint or redeem in a reentrant fashion - /// would not allow any theft of funds, it would simply build up a call stack - /// of orders that would need to be executed. - /// @param to recipient of the Volt - /// @param amountIn amount of underlying tokens used to purchase Volt - /// @param minAmountVoltOut minimum amount of Volt recipient to receive - function mint( - address to, - uint256 amountIn, - uint256 minAmountVoltOut - ) external override globalLock(1) returns (uint256 amountVoltOut) { - /// ------- Checks ------- - /// 1. current price from oracle is correct - /// 2. how much volt to receive - /// 3. volt to receive meets min amount out - - amountVoltOut = getMintAmountOut(amountIn); - require( - amountVoltOut >= minAmountVoltOut, - "PegStabilityModule: Mint not enough out" - ); - - /// ------- Check / Effect / Trusted Interaction ------- - - /// Checks that there is enough Volt left to mint globally. - /// This is a check as well, because if there isn't sufficient Volt to mint, - /// then, the call to mintVolt will fail in the RateLimitedV2 class. - globalRateLimitedMinter().mintVolt(to, amountVoltOut); /// Check, Effect, then Interaction with trusted contract - - /// ------- Interactions with Untrusted Contract ------- - - underlyingToken.safeTransferFrom(msg.sender, address(this), amountIn); /// Interaction -- untrusted contract - - emit Mint(to, amountIn, amountVoltOut); - } - - /// ----------- Public View-Only API ---------- - - /// @notice returns the maximum amount of Volt that can be minted - function getMaxMintAmountOut() external view returns (uint256) { - return globalRateLimitedMinter().buffer(); - } - - /// @notice returns the maximum amount of Volt that can be redeemed - /// with the current PSM balance - function getMaxRedeemAmountIn() external view override returns (uint256) { - /// usdc decimals normalizer: -12 - /// readOracle returns volt price / 1e12 - /// 1.06e18 / 1e12 = 1.06e6 - /// balance returns underlying token balance of usdc - /// 10_000e6 usdc - /// 10_000e6 * 1e18 / 1.06e6 - /// = 9.433962264E21 Volt - - /// dai decimals normalizer: 0 - /// readOracle returns volt price - /// 1.06e18 = 1.06e18 - /// balance returns underlying token balance of dai - /// 10_000e18 dai - /// 10_000e18 * 1e18 / 1.06e18 - /// = 9.433962264E21 Volt - - return (balance() * Constants.ETH_GRANULARITY) / readOracle(); - } -} diff --git a/src/rate-limits/GlobalRateLimitedMinter.sol b/src/rate-limits/GlobalRateLimitedMinter.sol index b17adccaa..a75485130 100644 --- a/src/rate-limits/GlobalRateLimitedMinter.sol +++ b/src/rate-limits/GlobalRateLimitedMinter.sol @@ -13,6 +13,7 @@ import {RateLimitedV2} from "@voltprotocol/utils/RateLimitedV2.sol"; /// Peg Stability Modules will be granted the RATE_LIMIT_SYSTEM_ENTRY_REPLENISH_ROLE to replenish /// this contract's on a global rate limit when burning Volt. contract GlobalRateLimitedMinter is IGlobalRateLimitedMinter, RateLimitedV2 { + /// @param _core reference to the core smart contract /// @param _maxRateLimitPerSecond maximum rate limit per second that governance can set /// @param _rateLimitPerSecond starting rate limit per second for Volt minting @@ -20,8 +21,8 @@ contract GlobalRateLimitedMinter is IGlobalRateLimitedMinter, RateLimitedV2 { constructor( address _core, uint256 _maxRateLimitPerSecond, - uint128 _rateLimitPerSecond, - uint128 _bufferCap + uint64 _rateLimitPerSecond, + uint96 _bufferCap ) CoreRefV2(_core) RateLimitedV2(_maxRateLimitPerSecond, _rateLimitPerSecond, _bufferCap) @@ -37,7 +38,7 @@ contract GlobalRateLimitedMinter is IGlobalRateLimitedMinter, RateLimitedV2 { ) external /// checks - onlyVoltRole(VoltRoles.RATE_LIMIT_SYSTEM_ENTRY_DEPLETE) + onlyVoltRole(VoltRoles.PSM_MINTER) /// system must be level 1 locked before this function can execute /// asserts system is inside PSM mint when this function is called globalLock(2) @@ -53,7 +54,7 @@ contract GlobalRateLimitedMinter is IGlobalRateLimitedMinter, RateLimitedV2 { ) external /// checks - onlyVoltRole(VoltRoles.RATE_LIMIT_SYSTEM_ENTRY_REPLENISH) + onlyVoltRole(VoltRoles.PSM_MINTER) /// system must be level 1 locked before this function can execute /// asserts system is inside PSM redeem when this function is called globalLock(2) @@ -61,3 +62,7 @@ contract GlobalRateLimitedMinter is IGlobalRateLimitedMinter, RateLimitedV2 { _replenishBuffer(amount); /// effects } } + +/// two goals: +/// 1. cap the Volt supply and creation of new Volt to 5.8m +/// 2. slow the redemption of Volt to only allow .5m per day to leave the system. diff --git a/src/rate-limits/GlobalSystemExitRateLimiter.sol b/src/rate-limits/GlobalSystemExitRateLimiter.sol deleted file mode 100644 index 0a74239e9..000000000 --- a/src/rate-limits/GlobalSystemExitRateLimiter.sol +++ /dev/null @@ -1,65 +0,0 @@ -pragma solidity =0.8.13; - -import {IGlobalSystemExitRateLimiter} from "@voltprotocol/rate-limits/IGlobalSystemExitRateLimiter.sol"; -import {CoreRefV2} from "@voltprotocol/refs/CoreRefV2.sol"; -import {VoltRoles} from "@voltprotocol/core/VoltRoles.sol"; -import {RateLimitedV2} from "@voltprotocol/utils/RateLimitedV2.sol"; - -/// @notice contract to control the flow of funds through the system with a rate limit. -/// In a bank run due to losses exceeding the surplus buffer, this will allow the system -/// to apply a uniform haircut to all users after the buffer is depleted. -/// All minting should flow through this smart contract. -/// Non Custodial Peg Stability Modules will be granted the RATE_LIMIT_SYSTEM_EXIT_DEPLETE_ROLE -/// to deplete the buffer through this contract on a global rate limit. -/// ERC20Allocator will be granted both the RATE_LIMIT_SYSTEM_EXIT_DEPLETE_ROLE and RATE_LIMIT_SYSTEM_EXIT_REPLENISH_ROLE -/// to be able to replenish and deplete the buffer. -contract GlobalSystemExitRateLimiter is - IGlobalSystemExitRateLimiter, - RateLimitedV2 -{ - /// @param _core reference to the core smart contract - /// @param _maxRateLimitPerSecond maximum rate limit per second that governance can set - /// @param _rateLimitPerSecond starting rate limit per second for Volt minting - /// @param _bufferCap cap on buffer size for this rate limited instance - constructor( - address _core, - uint256 _maxRateLimitPerSecond, - uint128 _rateLimitPerSecond, - uint128 _bufferCap - ) - CoreRefV2(_core) - RateLimitedV2(_maxRateLimitPerSecond, _rateLimitPerSecond, _bufferCap) - {} - - /// @notice anytime PCV is pulled from a PCV deposit where it can be redeemed, - /// or it is being redeemed, call in and deplete this buffer - /// Pausable and depletes the global buffer - /// @param amount the amount of dollars to deplete the buffer by - function depleteBuffer( - uint256 amount - ) - external - /// checks - onlyVoltRole(VoltRoles.RATE_LIMIT_SYSTEM_EXIT_DEPLETE) - /// system must be level 1 locked before this function can execute - /// asserts system is inside higher level operation when this function is called - globalLock(2) - { - _depleteBuffer(amount); /// check and effects - } - - /// @notice replenish buffer by amount of dollars sent to a PCV deposit - /// @param amount of dollars to replenish buffer by - function replenishBuffer( - uint256 amount - ) - external - /// checks - onlyVoltRole(VoltRoles.RATE_LIMIT_SYSTEM_EXIT_REPLENISH) - /// system must be level 1 locked before this function can execute - /// asserts system is inside higher level operation when this function is called - globalLock(2) - { - _replenishBuffer(amount); /// effects - } -} diff --git a/src/rate-limits/IGlobalSystemExitRateLimiter.sol b/src/rate-limits/IGlobalSystemExitRateLimiter.sol deleted file mode 100644 index d89fa84c3..000000000 --- a/src/rate-limits/IGlobalSystemExitRateLimiter.sol +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity =0.8.13; - -import {IRateLimitedV2} from "@voltprotocol/utils/IRateLimitedV2.sol"; - -interface IGlobalSystemExitRateLimiter is IRateLimitedV2 { - /// @notice anytime PCV is pulled from a PCV deposit where it can be redeemed, - /// or it is being redeemed, call in and deplete this buffer - /// Pausable and depletes the global buffer - /// @param amount the amount of dollars to deplete the buffer by - function depleteBuffer(uint256 amount) external; - - /// @notice replenish buffer by amount of dollars sent to a PCV deposit - /// @param amount of dollars to replenish buffer by - function replenishBuffer(uint256 amount) external; -} diff --git a/src/refs/CoreRefV2.sol b/src/refs/CoreRefV2.sol index 307066923..1fd663632 100644 --- a/src/refs/CoreRefV2.sol +++ b/src/refs/CoreRefV2.sol @@ -13,7 +13,6 @@ import {IPCVOracle} from "@voltprotocol/oracle/IPCVOracle.sol"; import {IVolt, IVoltBurn} from "@voltprotocol/volt/IVolt.sol"; import {IGlobalReentrancyLock} from "@voltprotocol/core/IGlobalReentrancyLock.sol"; import {IGlobalRateLimitedMinter} from "@voltprotocol/rate-limits/IGlobalRateLimitedMinter.sol"; -import {IGlobalSystemExitRateLimiter} from "@voltprotocol/rate-limits/IGlobalSystemExitRateLimiter.sol"; /// @title A Reference to Core /// @author Volt Protocol @@ -156,16 +155,6 @@ abstract contract CoreRefV2 is ICoreRefV2, Pausable { return _core.globalRateLimitedMinter(); } - /// @notice address of the GlobalSystemExitRateLimiter contract referenced by Core - /// @return IGlobalSystemExitRateLimiter implementation address - function globalSystemExitRateLimiter() - internal - view - returns (IGlobalSystemExitRateLimiter) - { - return _core.globalSystemExitRateLimiter(); - } - /// @notice address of the Global Reentrancy Lock contract reference /// @return address as type IGlobalReentrancyLock function globalReentrancyLock() diff --git a/src/refs/ICoreRefV2.sol b/src/refs/ICoreRefV2.sol index e712a8856..d08d7d9ad 100644 --- a/src/refs/ICoreRefV2.sol +++ b/src/refs/ICoreRefV2.sol @@ -8,7 +8,6 @@ import {IPCVOracle} from "@voltprotocol/oracle/IPCVOracle.sol"; import {IVolt, IVoltBurn} from "@voltprotocol/volt/IVolt.sol"; import {IGlobalReentrancyLock} from "@voltprotocol/core/IGlobalReentrancyLock.sol"; import {IGlobalRateLimitedMinter} from "@voltprotocol/rate-limits/IGlobalRateLimitedMinter.sol"; -import {IGlobalSystemExitRateLimiter} from "@voltprotocol/rate-limits/IGlobalSystemExitRateLimiter.sol"; /// @title CoreRef interface /// @author Volt Protocol diff --git a/src/utils/IRateLimitedV2.sol b/src/utils/IRateLimitedV2.sol index c74565d10..5488c5f4d 100644 --- a/src/utils/IRateLimitedV2.sol +++ b/src/utils/IRateLimitedV2.sol @@ -11,10 +11,14 @@ interface IRateLimitedV2 { function MAX_RATE_LIMIT_PER_SECOND() external view returns (uint256); /// @notice the rate per second for this contract - function rateLimitPerSecond() external view returns (uint128); + function rateLimitPerSecond() external view returns (uint64); /// @notice the cap of the buffer that can be used at once - function bufferCap() external view returns (uint128); + function bufferCap() external view returns (uint96); + + /// @notice buffer cap / 2 + /// this is the target for the buffer + function midPoint() external view returns (uint96); /// @notice the last time the buffer was used by the contract function lastBufferUsedTime() external view returns (uint32); @@ -29,10 +33,10 @@ interface IRateLimitedV2 { /// ------------- Governor Only API's ------------- /// @notice set the rate limit per second - function setRateLimitPerSecond(uint128 newRateLimitPerSecond) external; + function setRateLimitPerSecond(uint64 newRateLimitPerSecond) external; /// @notice set the buffer cap - function setBufferCap(uint128 newBufferCap) external; + function setBufferCap(uint96 newBufferCap) external; /// ------------- Events ------------- diff --git a/src/utils/RateLimitedV2.sol b/src/utils/RateLimitedV2.sol index 725f2669e..50f58b979 100644 --- a/src/utils/RateLimitedV2.sol +++ b/src/utils/RateLimitedV2.sol @@ -3,12 +3,21 @@ pragma solidity =0.8.13; import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {console} from "@forge-std/console.sol"; import {CoreRefV2} from "@voltprotocol/refs/CoreRefV2.sol"; import {IRateLimitedV2} from "@voltprotocol/utils/IRateLimitedV2.sol"; /// @title abstract contract for putting a rate limit on how fast a contract /// can perform an action e.g. Minting +/// Rate limit contract has a mid point that it tries to maintain. +/// When the stored buffer is above the mid point, time depletes the buffer +/// When the stored buffer is below the mid point, time replenishes the buffer +/// When buffer stored is at the mid point, do nothing +/// This contract is designed to allow both minting and redeeming +/// Mints deplete the buffer, and redeems replenish the buffer. +/// Deplete the buffer past 0 and execution reverts +/// Replenish the buffer past the buffer cap and execution reverts /// @author Elliot Friedman abstract contract RateLimitedV2 is IRateLimitedV2, CoreRefV2 { using SafeCast for *; @@ -19,10 +28,13 @@ abstract contract RateLimitedV2 is IRateLimitedV2, CoreRefV2 { /// ------------- First Storage Slot ------------- /// @notice the rate per second for this contract - uint128 public rateLimitPerSecond; + uint64 public rateLimitPerSecond; /// @notice the cap of the buffer that can be used at once - uint128 public bufferCap; + uint96 public bufferCap; + + /// @notice buffercap / 2 + uint96 public midPoint; /// ------------- Second Storage Slot ------------- @@ -38,11 +50,10 @@ abstract contract RateLimitedV2 is IRateLimitedV2, CoreRefV2 { /// @param _bufferCap cap on buffer size for this rate limited instance constructor( uint256 _maxRateLimitPerSecond, - uint128 _rateLimitPerSecond, - uint128 _bufferCap + uint64 _rateLimitPerSecond, + uint96 _bufferCap ) { lastBufferUsedTime = block.timestamp.toUint32(); - _setBufferCap(_bufferCap); bufferStored = _bufferCap; @@ -52,12 +63,13 @@ abstract contract RateLimitedV2 is IRateLimitedV2, CoreRefV2 { ); _setRateLimitPerSecond(_rateLimitPerSecond); + bufferStored = _bufferCap / 2; /// cached buffer starts at midpoint MAX_RATE_LIMIT_PER_SECOND = _maxRateLimitPerSecond; } /// @notice set the rate limit per second function setRateLimitPerSecond( - uint128 newRateLimitPerSecond + uint64 newRateLimitPerSecond ) external virtual onlyGovernor { require( newRateLimitPerSecond <= MAX_RATE_LIMIT_PER_SECOND, @@ -69,7 +81,7 @@ abstract contract RateLimitedV2 is IRateLimitedV2, CoreRefV2 { } /// @notice set the buffer cap - function setBufferCap(uint128 newBufferCap) external virtual onlyGovernor { + function setBufferCap(uint96 newBufferCap) external virtual onlyGovernor { _setBufferCap(newBufferCap); } @@ -77,8 +89,25 @@ abstract contract RateLimitedV2 is IRateLimitedV2, CoreRefV2 { /// @dev replenishes at rateLimitPerSecond per second up to bufferCap function buffer() public view returns (uint256) { uint256 elapsed = block.timestamp.toUint32() - lastBufferUsedTime; - return - Math.min(bufferStored + (rateLimitPerSecond * elapsed), bufferCap); + uint256 cachedBufferStored = bufferStored; + uint256 bufferDelta = rateLimitPerSecond * elapsed; + + console.log("midPoint: ", midPoint); + console.log("bufferDelta: ", bufferDelta); + console.log("bufferStored: ", cachedBufferStored); + + /// converge on mid point + if (cachedBufferStored < midPoint) { + /// buffer is below mid point, time accumulation can bring it back up to the mid point + return Math.min(cachedBufferStored + bufferDelta, midPoint); + } else if (cachedBufferStored > midPoint) { + /// buffer is above the mid point, time accumulation can bring it back down to the mid point + return Math.max(cachedBufferStored - bufferDelta, midPoint); + } + + console.log("returning buffer stored"); + /// if already at mid point, do nothing + return cachedBufferStored; } /// @notice the method that enforces the rate limit. @@ -97,29 +126,24 @@ abstract contract RateLimitedV2 is IRateLimitedV2, CoreRefV2 { lastBufferUsedTime = blockTimestamp; bufferStored = newBufferStored; - emit BufferUsed(amount, bufferStored); + emit BufferUsed(amount, newBufferStored); /// save single warm SLOAD with `newBufferStored` } /// @notice function to replenish buffer + /// cannot increase buffer if result would be gt buffer cap /// @param amount to increase buffer by if under buffer cap function _replenishBuffer(uint256 amount) internal { uint256 newBuffer = buffer(); uint256 _bufferCap = bufferCap; /// gas opti, save an SLOAD - /// cannot replenish any further if already at buffer cap - if (newBuffer == _bufferCap) { - /// save an SSTORE + some stack operations if buffer cannot be increased. - /// last buffer used time doesn't need to be updated as buffer cannot - /// increase past the buffer cap - return; - } + require(newBuffer + amount <= _bufferCap, "RateLimited: buffer cap overflow"); uint32 blockTimestamp = block.timestamp.toUint32(); - /// ensure that bufferStored cannot be gt buffer cap - uint224 newBufferStored = Math - .min(newBuffer + amount, _bufferCap) - .toUint224(); + + /// bufferStored cannot be gt buffer cap because of check + /// newBuffer + amount <= buffer cap + uint224 newBufferStored = uint224(newBuffer + amount); /// gas optimization to only use a single SSTORE lastBufferUsedTime = blockTimestamp; @@ -128,7 +152,7 @@ abstract contract RateLimitedV2 is IRateLimitedV2, CoreRefV2 { emit BufferReplenished(amount, bufferStored); } - function _setRateLimitPerSecond(uint128 newRateLimitPerSecond) internal { + function _setRateLimitPerSecond(uint64 newRateLimitPerSecond) internal { uint256 oldRateLimitPerSecond = rateLimitPerSecond; rateLimitPerSecond = newRateLimitPerSecond; @@ -138,11 +162,13 @@ abstract contract RateLimitedV2 is IRateLimitedV2, CoreRefV2 { ); } - function _setBufferCap(uint128 newBufferCap) internal { + function _setBufferCap(uint96 newBufferCap) internal { _updateBufferStored(); uint256 oldBufferCap = bufferCap; - bufferCap = newBufferCap; + uint96 newMidPoint = newBufferCap / 2; + midPoint = newMidPoint; /// start at midpoint + bufferCap = newBufferCap; /// set buffer cap emit BufferCapUpdate(oldBufferCap, newBufferCap); } diff --git a/test/integration/IntegrationTestMorphoCompoundPCVDeposit.t.sol b/test/integration/IntegrationTestMorphoCompoundPCVDeposit.t.sol index 161cbf10d..2b201fcaa 100644 --- a/test/integration/IntegrationTestMorphoCompoundPCVDeposit.t.sol +++ b/test/integration/IntegrationTestMorphoCompoundPCVDeposit.t.sol @@ -11,7 +11,6 @@ import {IPCVOracle} from "@voltprotocol/oracle/IPCVOracle.sol"; import {PCVGuardian} from "@voltprotocol/pcv/PCVGuardian.sol"; import {SystemEntry} from "@voltprotocol/entry/SystemEntry.sol"; import {GenericCallMock} from "@test/mock/GenericCallMock.sol"; -import {PegStabilityModule} from "@voltprotocol/peg/PegStabilityModule.sol"; import {MorphoCompoundPCVDeposit} from "@voltprotocol/pcv/morpho/MorphoCompoundPCVDeposit.sol"; import {TestAddresses as addresses} from "@test/unit/utils/TestAddresses.sol"; import {IGlobalReentrancyLock, GlobalReentrancyLock} from "@voltprotocol/core/GlobalReentrancyLock.sol"; diff --git a/test/integration/post-proposal-checks/IntegrationTestPCVOracle.sol b/test/integration/post-proposal-checks/IntegrationTestPCVOracle.sol index 66a80c956..637235423 100644 --- a/test/integration/post-proposal-checks/IntegrationTestPCVOracle.sol +++ b/test/integration/post-proposal-checks/IntegrationTestPCVOracle.sol @@ -11,17 +11,17 @@ import {PCVOracle} from "@voltprotocol/oracle/PCVOracle.sol"; import {IPCVOracle} from "@voltprotocol/oracle/IPCVOracle.sol"; import {PCVGuardian} from "@voltprotocol/pcv/PCVGuardian.sol"; import {GenericCallMock} from "@test/mock/GenericCallMock.sol"; -import {PegStabilityModule} from "@voltprotocol/peg/PegStabilityModule.sol"; +import {NonCustodialPSM} from "@voltprotocol/peg/NonCustodialPSM.sol"; contract IntegrationTestPCVOracle is PostProposalCheck { CoreV2 private core; IERC20 private dai; VoltV2 private volt; address private grlm; - address private morphoDaiPCVDeposit; - PegStabilityModule private daipsm; PCVOracle private pcvOracle; + NonCustodialPSM private daipsm; PCVGuardian private pcvGuardian; + address private morphoDaiPCVDeposit; function setUp() public override { super.setUp(); @@ -33,7 +33,7 @@ contract IntegrationTestPCVOracle is PostProposalCheck { morphoDaiPCVDeposit = addresses.mainnet( "PCV_DEPOSIT_MORPHO_COMPOUND_DAI" ); - daipsm = PegStabilityModule(addresses.mainnet("PSM_DAI")); + daipsm = NonCustodialPSM(addresses.mainnet("PSM_NONCUSTODIAL_DAI")); pcvOracle = PCVOracle(addresses.mainnet("PCV_ORACLE")); pcvGuardian = PCVGuardian(addresses.mainnet("PCV_GUARDIAN")); } diff --git a/test/integration/post-proposal-checks/IntegrationTestRateLimiters.sol b/test/integration/post-proposal-checks/IntegrationTestRateLimiters.sol index 3b7937891..46cf0a95f 100644 --- a/test/integration/post-proposal-checks/IntegrationTestRateLimiters.sol +++ b/test/integration/post-proposal-checks/IntegrationTestRateLimiters.sol @@ -12,11 +12,8 @@ import {CoreV2} from "@voltprotocol/core/CoreV2.sol"; import {PCVOracle} from "@voltprotocol/oracle/PCVOracle.sol"; import {PCVDeposit} from "@voltprotocol/pcv/PCVDeposit.sol"; import {SystemEntry} from "@voltprotocol/entry/SystemEntry.sol"; -import {ERC20Allocator} from "@voltprotocol/pcv/ERC20Allocator.sol"; import {NonCustodialPSM} from "@voltprotocol/peg/NonCustodialPSM.sol"; import {GlobalRateLimitedMinter} from "@voltprotocol/rate-limits/GlobalRateLimitedMinter.sol"; -import {GlobalSystemExitRateLimiter} from "@voltprotocol/rate-limits/GlobalSystemExitRateLimiter.sol"; -import {PegStabilityModule} from "@voltprotocol/peg/PegStabilityModule.sol"; contract IntegrationTestRateLimiters is PostProposalCheck { using SafeCast for *; @@ -24,21 +21,17 @@ contract IntegrationTestRateLimiters is PostProposalCheck { address public constant user = 0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7; uint256 public snapshotAfterMints; + IERC20 private dai; + IERC20 private usdc; CoreV2 private core; VoltV2 private volt; + PCVOracle private pcvOracle; SystemEntry private systemEntry; - ERC20Allocator private allocator; - PegStabilityModule private usdcpsm; - PegStabilityModule private daipsm; - NonCustodialPSM private usdcncpsm; NonCustodialPSM private daincpsm; - IERC20 private dai; - IERC20 private usdc; + NonCustodialPSM private usdcncpsm; GlobalRateLimitedMinter private grlm; - GlobalSystemExitRateLimiter private gserl; - PCVOracle private pcvOracle; - address private morphoUsdcPCVDeposit; address private morphoDaiPCVDeposit; + address private morphoUsdcPCVDeposit; function setUp() public override { super.setUp(); @@ -46,19 +39,13 @@ contract IntegrationTestRateLimiters is PostProposalCheck { core = CoreV2(addresses.mainnet("CORE")); volt = VoltV2(addresses.mainnet("VOLT")); systemEntry = SystemEntry(addresses.mainnet("SYSTEM_ENTRY")); - allocator = ERC20Allocator(addresses.mainnet("PSM_ALLOCATOR")); - usdcpsm = PegStabilityModule(addresses.mainnet("PSM_USDC")); - daipsm = PegStabilityModule(addresses.mainnet("PSM_DAI")); - usdcncpsm = NonCustodialPSM(addresses.mainnet("PSM_NONCUSTODIAL_USDC")); daincpsm = NonCustodialPSM(addresses.mainnet("PSM_NONCUSTODIAL_DAI")); + usdcncpsm = NonCustodialPSM(addresses.mainnet("PSM_NONCUSTODIAL_USDC")); dai = IERC20(addresses.mainnet("DAI")); usdc = IERC20(addresses.mainnet("USDC")); grlm = GlobalRateLimitedMinter( addresses.mainnet("GLOBAL_RATE_LIMITED_MINTER") ); - gserl = GlobalSystemExitRateLimiter( - addresses.mainnet("GLOBAL_SYSTEM_EXIT_RATE_LIMITER") - ); pcvOracle = PCVOracle(addresses.mainnet("PCV_ORACLE")); morphoUsdcPCVDeposit = addresses.mainnet( "PCV_DEPOSIT_MORPHO_COMPOUND_USDC" @@ -79,98 +66,32 @@ contract IntegrationTestRateLimiters is PostProposalCheck { // number of moved funds for tests uint256 amount = initialBuffer / 2; - (, uint256 daiPSMTargetBalance, ) = allocator.allPSMs( - address(daipsm) - ); - (, uint256 usdcPSMTargetBalance, ) = allocator.allPSMs( - address(usdcpsm) - ); // read initial pcv uint256 startLiquidPcv = pcvOracle.getTotalPcv(); - // read initial psm balances - uint256 startPsmDaiBalance = dai.balanceOf(address(daipsm)); - uint256 startPsmUsdcBalance = usdc.balanceOf(address(usdcpsm)); // user performs the first mint with DAI vm.startPrank(user); - dai.approve(address(daipsm), amount); - daipsm.mint(user, amount, 0); + dai.approve(address(daincpsm), amount); + daincpsm.mint(user, amount, 0); vm.stopPrank(); - (, uint256 adjustedSkimAmount) = allocator.getSkimDetails( - address(morphoDaiPCVDeposit) - ); - uint256 gserlStartingBuffer = gserl.buffer(); - // buffer has been used uint256 voltReceived1 = volt.balanceOf(user); assertEq(grlm.buffer(), initialBuffer - voltReceived1); - allocator.skim(morphoDaiPCVDeposit); - - /// assert replenish occurred if not maxed out - assertEq( - Math.min( - gserlStartingBuffer + adjustedSkimAmount, - gserlStartingBuffer - ), - gserl.buffer() - ); - - // after first mint, pcv increased by amount - uint256 liquidPcv2 = pcvOracle.getTotalPcv(); - assertApproxEq( - liquidPcv2.toInt256(), - (startLiquidPcv + - startPsmDaiBalance + - amount - - daiPSMTargetBalance).toInt256(), - 0 - ); - // user performs the second mint wit USDC vm.startPrank(user); - usdc.approve(address(usdcpsm), amount / 1e12); - usdcpsm.mint(user, amount / 1e12, 0); + usdc.approve(address(usdcncpsm), amount / 1e12); + usdcncpsm.mint(user, amount / 1e12, 0); vm.stopPrank(); uint256 voltReceived2 = volt.balanceOf(user) - voltReceived1; - (, adjustedSkimAmount) = allocator.getSkimDetails( - address(morphoUsdcPCVDeposit) - ); - gserlStartingBuffer = gserl.buffer(); - // buffer has been used assertEq( grlm.buffer(), initialBuffer - voltReceived1 - voltReceived2 ); - - allocator.skim(morphoUsdcPCVDeposit); - { - // after second mint, pcv is = 2 * amount - uint256 liquidPcv3 = pcvOracle.getTotalPcv(); - assertApproxEq( - liquidPcv3.toInt256(), - (liquidPcv2 + - startPsmUsdcBalance * - 1e12 + - amount - - usdcPSMTargetBalance * - 1e12).toInt256(), - 0 - ); - } - - /// assert replenish occurred if not maxed out - assertEq( - Math.min( - gserlStartingBuffer + adjustedSkimAmount, - gserlStartingBuffer - ), - gserl.buffer() - ); } snapshotAfterMints = vm.snapshot(); @@ -184,9 +105,9 @@ contract IntegrationTestRateLimiters is PostProposalCheck { // above limit rate reverts uint256 largeAmount = grlm.bufferCap() * 2; vm.startPrank(user); - dai.approve(address(daipsm), largeAmount); + dai.approve(address(daincpsm), largeAmount); vm.expectRevert("RateLimited: rate limit hit"); - daipsm.mint(user, largeAmount, 0); + daincpsm.mint(user, largeAmount, 0); vm.stopPrank(); } @@ -196,16 +117,17 @@ contract IntegrationTestRateLimiters is PostProposalCheck { vm.revertTo(snapshotAfterMints); vm.assume(voltAmount <= volt.balanceOf(user)); - uint256 daiAmountOut = daipsm.getRedeemAmountOut(voltAmount); - deal(address(dai), address(daipsm), daiAmountOut); + uint256 daiAmountOut = daincpsm.getRedeemAmountOut(voltAmount); + deal(address(dai), morphoDaiPCVDeposit, daiAmountOut); + systemEntry.deposit(morphoDaiPCVDeposit); vm.startPrank(user); uint256 startingBuffer = grlm.buffer(); uint256 startingDaiBalance = dai.balanceOf(user); - volt.approve(address(daipsm), voltAmount); - daipsm.redeem(user, voltAmount, daiAmountOut); + volt.approve(address(daincpsm), voltAmount); + daincpsm.redeem(user, voltAmount, daiAmountOut); uint256 endingBuffer = grlm.buffer(); uint256 endingDaiBalance = dai.balanceOf(user); @@ -214,14 +136,6 @@ contract IntegrationTestRateLimiters is PostProposalCheck { assertEq(endingBuffer - startingBuffer, voltAmount); vm.stopPrank(); - - uint256 startingExitBuffer = gserl.buffer(); - (, uint256 expectedBufferDepletion) = allocator.getDripDetails( - address(daipsm), - address(morphoDaiPCVDeposit) - ); - allocator.drip(address(morphoDaiPCVDeposit)); - assertEq(startingExitBuffer - expectedBufferDepletion, gserl.buffer()); } function testRedeemsDaiNcPsm(uint80 voltRedeemAmount) public { @@ -235,24 +149,20 @@ contract IntegrationTestRateLimiters is PostProposalCheck { deal(address(dai), morphoDaiPCVDeposit, daiAmountOut * 2); systemEntry.deposit(morphoDaiPCVDeposit); - vm.startPrank(user); uint256 startingBuffer = grlm.buffer(); - uint256 startingExitBuffer = gserl.buffer(); uint256 startingDaiBalance = dai.balanceOf(user); + vm.startPrank(user); volt.approve(address(daincpsm), voltRedeemAmount); daincpsm.redeem(user, voltRedeemAmount, daiAmountOut); + vm.stopPrank(); uint256 endingBuffer = grlm.buffer(); - uint256 endingExitBuffer = gserl.buffer(); uint256 endingDaiBalance = dai.balanceOf(user); assertEq(endingBuffer - startingBuffer, voltRedeemAmount); /// grlm buffer replenished assertEq(endingDaiBalance - startingDaiBalance, daiAmountOut); - assertEq(startingExitBuffer - endingExitBuffer, daiAmountOut); /// exit buffer depleted - - vm.stopPrank(); } function testRedeemsUsdcNcPsm(uint80 voltRedeemAmount) public { @@ -269,19 +179,16 @@ contract IntegrationTestRateLimiters is PostProposalCheck { vm.startPrank(user); uint256 startingBuffer = grlm.buffer(); - uint256 startingExitBuffer = gserl.buffer(); uint256 startingUsdcBalance = usdc.balanceOf(user); volt.approve(address(usdcncpsm), voltRedeemAmount); usdcncpsm.redeem(user, voltRedeemAmount, usdcAmountOut); uint256 endingBuffer = grlm.buffer(); - uint256 endingExitBuffer = gserl.buffer(); uint256 endingUsdcBalance = usdc.balanceOf(user); assertEq(endingBuffer - startingBuffer, voltRedeemAmount); /// buffer replenished assertEq(endingUsdcBalance - startingUsdcBalance, usdcAmountOut); - assertEq(startingExitBuffer - endingExitBuffer, usdcAmountOut * 1e12); /// ensure buffer adjusted up 12 decimals, buffer depleted vm.stopPrank(); } @@ -293,36 +200,16 @@ contract IntegrationTestRateLimiters is PostProposalCheck { vm.revertTo(snapshotAfterMints); - uint256 usdcAmountOut = usdcpsm.getRedeemAmountOut(voltRedeemAmount); - deal(address(usdc), address(usdcpsm), usdcAmountOut); + uint256 usdcAmountOut = usdcncpsm.getRedeemAmountOut(voltRedeemAmount); + deal(address(usdc), morphoUsdcPCVDeposit, usdcAmountOut); + systemEntry.deposit(morphoUsdcPCVDeposit); uint256 startingBuffer = grlm.buffer(); - uint256 startingExitBuffer = gserl.buffer(); uint256 startingUsdcBalance = usdc.balanceOf(user); vm.startPrank(user); - volt.approve(address(usdcpsm), voltRedeemAmount); - usdcpsm.redeem(user, voltRedeemAmount, usdcAmountOut); + volt.approve(address(usdcncpsm), voltRedeemAmount); + usdcncpsm.redeem(user, voltRedeemAmount, usdcAmountOut); vm.stopPrank(); - - (, uint256 expectedBufferDepletion) = allocator.getDripDetails( - address(usdcpsm), - address(morphoUsdcPCVDeposit) - ); - allocator.drip(address(morphoUsdcPCVDeposit)); - uint256 endingExitBuffer = gserl.buffer(); - - assertEq( - startingExitBuffer - endingExitBuffer, - expectedBufferDepletion - ); /// ensure buffer adjusted up 12 decimals, buffer depleted - - assertEq(expectedBufferDepletion, gserl.bufferCap() - gserl.buffer()); - - uint256 endingBuffer = grlm.buffer(); - uint256 endingUsdcBalance = usdc.balanceOf(user); - - assertEq(endingBuffer - startingBuffer, voltRedeemAmount); - assertEq(endingUsdcBalance - startingUsdcBalance, usdcAmountOut); } } diff --git a/test/integration/post-proposal-checks/IntegrationTestRoles.sol b/test/integration/post-proposal-checks/IntegrationTestRoles.sol index e6b38b3d5..cdbeea4ed 100644 --- a/test/integration/post-proposal-checks/IntegrationTestRoles.sol +++ b/test/integration/post-proposal-checks/IntegrationTestRoles.sol @@ -31,29 +31,26 @@ contract IntegrationTestRoles is PostProposalCheck { core.getRoleAdmin(VoltRoles.PCV_CONTROLLER), VoltRoles.GOVERNOR ); - assertEq(core.getRoleMemberCount(VoltRoles.PCV_CONTROLLER), 6); + assertEq(core.getRoleMemberCount(VoltRoles.PCV_CONTROLLER), 5); + assertEq( core.getRoleMember(VoltRoles.PCV_CONTROLLER, 0), - addresses.mainnet("PSM_ALLOCATOR") - ); - assertEq( - core.getRoleMember(VoltRoles.PCV_CONTROLLER, 1), addresses.mainnet("PCV_GUARDIAN") ); assertEq( - core.getRoleMember(VoltRoles.PCV_CONTROLLER, 2), + core.getRoleMember(VoltRoles.PCV_CONTROLLER, 1), addresses.mainnet("PCV_ROUTER") ); assertEq( - core.getRoleMember(VoltRoles.PCV_CONTROLLER, 3), + core.getRoleMember(VoltRoles.PCV_CONTROLLER, 2), addresses.mainnet("GOVERNOR") ); assertEq( - core.getRoleMember(VoltRoles.PCV_CONTROLLER, 4), + core.getRoleMember(VoltRoles.PCV_CONTROLLER, 3), addresses.mainnet("PSM_NONCUSTODIAL_DAI") ); assertEq( - core.getRoleMember(VoltRoles.PCV_CONTROLLER, 5), + core.getRoleMember(VoltRoles.PCV_CONTROLLER, 4), addresses.mainnet("PSM_NONCUSTODIAL_USDC") ); @@ -125,121 +122,79 @@ contract IntegrationTestRoles is PostProposalCheck { addresses.mainnet("COMPOUND_BAD_DEBT_SENTINEL") ); - // RATE_LIMIT_SYSTEM_ENTRY_DEPLETE_ROLE - assertEq( - core.getRoleAdmin(VoltRoles.RATE_LIMIT_SYSTEM_ENTRY_DEPLETE), - VoltRoles.GOVERNOR - ); - assertEq( - core.getRoleMemberCount(VoltRoles.RATE_LIMIT_SYSTEM_ENTRY_DEPLETE), - 2 - ); - assertEq( - core.getRoleMember(VoltRoles.RATE_LIMIT_SYSTEM_ENTRY_DEPLETE, 0), - addresses.mainnet("PSM_DAI") - ); - assertEq( - core.getRoleMember(VoltRoles.RATE_LIMIT_SYSTEM_ENTRY_DEPLETE, 1), - addresses.mainnet("PSM_USDC") - ); - - // RATE_LIMIT_SYSTEM_ENTRY_REPLENISH_ROLE + // PSM_MINTER_ROLE assertEq( - core.getRoleAdmin(VoltRoles.RATE_LIMIT_SYSTEM_ENTRY_REPLENISH), + core.getRoleAdmin(VoltRoles.PSM_MINTER), VoltRoles.GOVERNOR ); assertEq( core.getRoleMemberCount( - VoltRoles.RATE_LIMIT_SYSTEM_ENTRY_REPLENISH + VoltRoles.PSM_MINTER ), - 4 - ); - assertEq( - core.getRoleMember(VoltRoles.RATE_LIMIT_SYSTEM_ENTRY_REPLENISH, 0), - addresses.mainnet("PSM_DAI") - ); - assertEq( - core.getRoleMember(VoltRoles.RATE_LIMIT_SYSTEM_ENTRY_REPLENISH, 1), - addresses.mainnet("PSM_USDC") + 2 ); assertEq( - core.getRoleMember(VoltRoles.RATE_LIMIT_SYSTEM_ENTRY_REPLENISH, 2), + core.getRoleMember(VoltRoles.PSM_MINTER, 0), addresses.mainnet("PSM_NONCUSTODIAL_DAI") ); assertEq( - core.getRoleMember(VoltRoles.RATE_LIMIT_SYSTEM_ENTRY_REPLENISH, 3), + core.getRoleMember(VoltRoles.PSM_MINTER, 1), addresses.mainnet("PSM_NONCUSTODIAL_USDC") ); // LOCKER_ROLE assertEq(core.getRoleAdmin(VoltRoles.LOCKER), VoltRoles.GOVERNOR); - assertEq(core.getRoleMemberCount(VoltRoles.LOCKER), 17); + assertEq(core.getRoleMemberCount(VoltRoles.LOCKER), 13); assertEq( core.getRoleMember(VoltRoles.LOCKER, 0), addresses.mainnet("SYSTEM_ENTRY") ); assertEq( core.getRoleMember(VoltRoles.LOCKER, 1), - addresses.mainnet("PSM_ALLOCATOR") - ); - assertEq( - core.getRoleMember(VoltRoles.LOCKER, 2), addresses.mainnet("PCV_ORACLE") ); assertEq( - core.getRoleMember(VoltRoles.LOCKER, 3), - addresses.mainnet("PSM_DAI") - ); - assertEq( - core.getRoleMember(VoltRoles.LOCKER, 4), - addresses.mainnet("PSM_USDC") - ); - assertEq( - core.getRoleMember(VoltRoles.LOCKER, 5), + core.getRoleMember(VoltRoles.LOCKER, 2), addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_DAI") ); assertEq( - core.getRoleMember(VoltRoles.LOCKER, 6), + core.getRoleMember(VoltRoles.LOCKER, 3), addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_USDC") ); assertEq( - core.getRoleMember(VoltRoles.LOCKER, 7), + core.getRoleMember(VoltRoles.LOCKER, 4), addresses.mainnet("GLOBAL_RATE_LIMITED_MINTER") ); assertEq( - core.getRoleMember(VoltRoles.LOCKER, 8), - addresses.mainnet("GLOBAL_SYSTEM_EXIT_RATE_LIMITER") - ); - assertEq( - core.getRoleMember(VoltRoles.LOCKER, 9), + core.getRoleMember(VoltRoles.LOCKER, 5), addresses.mainnet("PCV_ROUTER") ); assertEq( - core.getRoleMember(VoltRoles.LOCKER, 10), + core.getRoleMember(VoltRoles.LOCKER, 6), addresses.mainnet("PCV_GUARDIAN") ); assertEq( - core.getRoleMember(VoltRoles.LOCKER, 11), + core.getRoleMember(VoltRoles.LOCKER, 7), addresses.mainnet("PSM_NONCUSTODIAL_DAI") ); assertEq( - core.getRoleMember(VoltRoles.LOCKER, 12), + core.getRoleMember(VoltRoles.LOCKER, 8), addresses.mainnet("PSM_NONCUSTODIAL_USDC") ); assertEq( - core.getRoleMember(VoltRoles.LOCKER, 13), + core.getRoleMember(VoltRoles.LOCKER, 9), addresses.mainnet("PCV_DEPOSIT_EULER_DAI") ); assertEq( - core.getRoleMember(VoltRoles.LOCKER, 14), + core.getRoleMember(VoltRoles.LOCKER, 10), addresses.mainnet("PCV_DEPOSIT_EULER_USDC") ); assertEq( - core.getRoleMember(VoltRoles.LOCKER, 15), + core.getRoleMember(VoltRoles.LOCKER, 11), addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_DAI") ); assertEq( - core.getRoleMember(VoltRoles.LOCKER, 16), + core.getRoleMember(VoltRoles.LOCKER, 12), addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_USDC") ); @@ -250,41 +205,5 @@ contract IntegrationTestRoles is PostProposalCheck { core.getRoleMember(VoltRoles.MINTER, 0), addresses.mainnet("GLOBAL_RATE_LIMITED_MINTER") ); - - /// SYSTEM EXIT RATE LIMIT DEPLETER - assertEq( - core.getRoleAdmin(VoltRoles.RATE_LIMIT_SYSTEM_EXIT_DEPLETE), - VoltRoles.GOVERNOR - ); - assertEq( - core.getRoleMemberCount(VoltRoles.RATE_LIMIT_SYSTEM_EXIT_DEPLETE), - 3 - ); - assertEq( - core.getRoleMember(VoltRoles.RATE_LIMIT_SYSTEM_EXIT_DEPLETE, 0), - addresses.mainnet("PSM_ALLOCATOR") - ); - assertEq( - core.getRoleMember(VoltRoles.RATE_LIMIT_SYSTEM_EXIT_DEPLETE, 1), - addresses.mainnet("PSM_NONCUSTODIAL_DAI") - ); - assertEq( - core.getRoleMember(VoltRoles.RATE_LIMIT_SYSTEM_EXIT_DEPLETE, 2), - addresses.mainnet("PSM_NONCUSTODIAL_USDC") - ); - - /// SYSTEM EXIT RATE LIMIT REPLENISH - assertEq( - core.getRoleAdmin(VoltRoles.RATE_LIMIT_SYSTEM_EXIT_REPLENISH), - VoltRoles.GOVERNOR - ); - assertEq( - core.getRoleMemberCount(VoltRoles.RATE_LIMIT_SYSTEM_EXIT_REPLENISH), - 1 - ); - assertEq( - core.getRoleMember(VoltRoles.RATE_LIMIT_SYSTEM_EXIT_REPLENISH, 0), - addresses.mainnet("PSM_ALLOCATOR") - ); } } diff --git a/test/integration/post-proposal-checks/IntegrationTestUsePSMs.sol b/test/integration/post-proposal-checks/IntegrationTestUsePSMs.sol deleted file mode 100644 index c88d0da44..000000000 --- a/test/integration/post-proposal-checks/IntegrationTestUsePSMs.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.13; - -import {PostProposalCheck} from "@test/integration/post-proposal-checks/PostProposalCheck.sol"; - -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {PegStabilityModule} from "@voltprotocol/peg/PegStabilityModule.sol"; - -// Tests that mint & redeem on all PSMS do not revert. -// Does not make any assumptions about the VOLT rate. -contract IntegrationTestUsePSMs is PostProposalCheck { - uint256 private constant AMOUNT = 5_000; - - function testMainnetUSDCMintRedeem() public { - PegStabilityModule psm = PegStabilityModule( - addresses.mainnet("PSM_USDC") - ); - IERC20 volt = IERC20(addresses.mainnet("VOLT")); - IERC20 token = IERC20(addresses.mainnet("USDC")); - uint256 amountTokens = AMOUNT * 1e6; - - // mock non-zero balance of tokens for user - deal(address(token), address(this), amountTokens); - - // do mint - token.approve(address(psm), amountTokens); - psm.mint(address(this), amountTokens, 0); - - // check received volt - uint256 receivedVolt = volt.balanceOf(address(this)); - assertTrue(receivedVolt > 0); - - // do redeem - volt.approve(address(psm), receivedVolt); - psm.redeem(address(this), receivedVolt, 0); - - // check received tokens (tolerance of 1 wei for round-down) - assertTrue(token.balanceOf(address(this)) >= amountTokens - 1); - } - - function testMainnetDAIMintRedeem() public { - PegStabilityModule psm = PegStabilityModule( - addresses.mainnet("PSM_DAI") - ); - IERC20 volt = IERC20(addresses.mainnet("VOLT")); - IERC20 token = IERC20(addresses.mainnet("DAI")); - uint256 amountTokens = AMOUNT * 1e18; - - // mock non-zero balance of tokens for user - deal(address(token), address(this), amountTokens); - - // do mint - token.approve(address(psm), amountTokens); - psm.mint(address(this), amountTokens, 0); - - // check received volt - uint256 receivedVolt = volt.balanceOf(address(this)); - assertTrue(receivedVolt > 0); - - // do redeem - volt.approve(address(psm), receivedVolt); - psm.redeem(address(this), receivedVolt, 0); - - // check received tokens (tolerance of 1 wei for round-down) - assertTrue(token.balanceOf(address(this)) >= amountTokens - 1); - } -} diff --git a/test/integration/post-proposal-checks/IntegrationTestVoltV1Migration.sol b/test/integration/post-proposal-checks/IntegrationTestVoltV1Migration.sol index 888132159..5f68808de 100644 --- a/test/integration/post-proposal-checks/IntegrationTestVoltV1Migration.sol +++ b/test/integration/post-proposal-checks/IntegrationTestVoltV1Migration.sol @@ -1,510 +1,509 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.13; - -import {PostProposalCheck} from "@test/integration/post-proposal-checks/PostProposalCheck.sol"; - -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; -import {TimelockController} from "@openzeppelin/contracts/governance/TimelockController.sol"; - -import {IVolt} from "@voltprotocol/volt/IVolt.sol"; -import {VoltV2} from "@voltprotocol/volt/VoltV2.sol"; -import {CoreV2} from "@voltprotocol/core/CoreV2.sol"; -import {stdError} from "@forge-std/StdError.sol"; -import {MigratorRouter} from "@voltprotocol/v1-migration/MigratorRouter.sol"; -import {VoltSystemOracle} from "@voltprotocol/oracle/VoltSystemOracle.sol"; -import {PegStabilityModule} from "@voltprotocol/peg/PegStabilityModule.sol"; -import {IVoltMigrator, VoltMigrator} from "@voltprotocol/v1-migration/VoltMigrator.sol"; - -contract IntegrationTestVoltV1Migration is PostProposalCheck { - using SafeCast for *; - - uint224 public constant mintAmount = 100_000_000e18; - - TimelockController private timelockController; - IVolt private oldVolt; - VoltV2 private volt; - VoltMigrator private voltMigrator; - MigratorRouter private migratorRouter; - PegStabilityModule private usdcpsm; - PegStabilityModule private daipsm; - IERC20 private dai; - IERC20 private usdc; - VoltSystemOracle private vso; - address private grlm; - address private multisig; - address private coreV1; - - function setUp() public override { - super.setUp(); - - timelockController = TimelockController( - payable(addresses.mainnet("TIMELOCK_CONTROLLER")) - ); - oldVolt = IVolt(addresses.mainnet("V1_VOLT")); - volt = VoltV2(addresses.mainnet("VOLT")); - voltMigrator = VoltMigrator(addresses.mainnet("V1_MIGRATION_MIGRATOR")); - migratorRouter = MigratorRouter( - addresses.mainnet("V1_MIGRATION_ROUTER") - ); - usdcpsm = PegStabilityModule(addresses.mainnet("PSM_USDC")); - daipsm = PegStabilityModule(addresses.mainnet("PSM_DAI")); - dai = IERC20(addresses.mainnet("DAI")); - usdc = IERC20(addresses.mainnet("USDC")); - vso = VoltSystemOracle(addresses.mainnet("VOLT_SYSTEM_ORACLE")); - grlm = addresses.mainnet("GLOBAL_RATE_LIMITED_MINTER"); - multisig = addresses.mainnet("GOVERNOR"); - coreV1 = addresses.mainnet("V1_CORE"); - } - - function testExchangeTo(uint64 amountOldVoltToExchange) public { - oldVolt.approve(address(voltMigrator), type(uint256).max); - deal(address(oldVolt), address(this), amountOldVoltToExchange); - deal(address(volt), address(voltMigrator), amountOldVoltToExchange); - - uint256 newVoltTotalSupply = volt.totalSupply(); - uint256 oldVoltTotalSupply = oldVolt.totalSupply(); - - uint256 oldVoltBalanceBefore = oldVolt.balanceOf(address(this)); - uint256 newVoltBalanceBefore = volt.balanceOf(address(0xFFF)); - - voltMigrator.exchangeTo(address(0xFFF), amountOldVoltToExchange); - - uint256 oldVoltBalanceAfter = oldVolt.balanceOf(address(this)); - uint256 newVoltBalanceAfter = volt.balanceOf(address(0xFFF)); - - assertEq( - newVoltBalanceAfter, - newVoltBalanceBefore + amountOldVoltToExchange - ); - assertEq( - oldVoltBalanceAfter, - oldVoltBalanceBefore - amountOldVoltToExchange - ); - assertEq( - oldVolt.totalSupply(), - oldVoltTotalSupply - amountOldVoltToExchange - ); - assertEq(volt.totalSupply(), newVoltTotalSupply); /// new volt supply remains unchanged - } - - function testExchangeAllTo() public { - uint256 amountOldVoltToExchange = 10_000e18; - - oldVolt.approve(address(voltMigrator), type(uint256).max); - - deal(address(oldVolt), address(this), amountOldVoltToExchange); - deal(address(volt), address(voltMigrator), amountOldVoltToExchange); - - uint256 newVoltTotalSupply = volt.totalSupply(); - uint256 oldVoltTotalSupply = oldVolt.totalSupply(); - - uint256 oldVoltBalanceBefore = oldVolt.balanceOf(address(this)); - uint256 newVoltBalanceBefore = volt.balanceOf(address(0xFFF)); - - voltMigrator.exchangeAllTo(address(0xFFF)); - - uint256 oldVoltBalanceAfter = oldVolt.balanceOf(address(this)); - uint256 newVoltBalanceAfter = volt.balanceOf(address(0xFFF)); - - assertEq( - newVoltBalanceAfter, - newVoltBalanceBefore + oldVoltBalanceBefore - ); - assertEq(oldVoltBalanceAfter, 0); - assertEq( - oldVolt.totalSupply(), - oldVoltTotalSupply - oldVoltBalanceBefore - ); - assertEq(volt.totalSupply(), newVoltTotalSupply); - } - - function testExchangeFailsWhenApprovalNotGiven() public { - vm.expectRevert("ERC20: burn amount exceeds allowance"); - voltMigrator.exchange(1e18); - } - - function testExchangeToFailsWhenApprovalNotGiven() public { - vm.expectRevert("ERC20: burn amount exceeds allowance"); - voltMigrator.exchangeTo(address(0xFFF), 1e18); - } - - function testExchangeFailsMigratorUnderfunded() public { - uint256 amountOldVoltToExchange = 100_000_000e18; +// // SPDX-License-Identifier: GPL-3.0-or-later +// pragma solidity 0.8.13; + +// import {PostProposalCheck} from "@test/integration/post-proposal-checks/PostProposalCheck.sol"; + +// import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +// import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +// import {TimelockController} from "@openzeppelin/contracts/governance/TimelockController.sol"; + +// import {IVolt} from "@voltprotocol/volt/IVolt.sol"; +// import {VoltV2} from "@voltprotocol/volt/VoltV2.sol"; +// import {CoreV2} from "@voltprotocol/core/CoreV2.sol"; +// import {stdError} from "@forge-std/StdError.sol"; +// import {MigratorRouter} from "@voltprotocol/v1-migration/MigratorRouter.sol"; +// import {VoltSystemOracle} from "@voltprotocol/oracle/VoltSystemOracle.sol"; +// import {IVoltMigrator, VoltMigrator} from "@voltprotocol/v1-migration/VoltMigrator.sol"; + +// contract IntegrationTestVoltV1Migration is PostProposalCheck { +// using SafeCast for *; + +// uint224 public constant mintAmount = 100_000_000e18; + +// TimelockController private timelockController; +// IVolt private oldVolt; +// VoltV2 private volt; +// VoltMigrator private voltMigrator; +// MigratorRouter private migratorRouter; +// PegStabilityModule private usdcpsm; +// PegStabilityModule private daipsm; +// IERC20 private dai; +// IERC20 private usdc; +// VoltSystemOracle private vso; +// address private grlm; +// address private multisig; +// address private coreV1; + +// function setUp() public override { +// super.setUp(); + +// timelockController = TimelockController( +// payable(addresses.mainnet("TIMELOCK_CONTROLLER")) +// ); +// oldVolt = IVolt(addresses.mainnet("V1_VOLT")); +// volt = VoltV2(addresses.mainnet("VOLT")); +// voltMigrator = VoltMigrator(addresses.mainnet("V1_MIGRATION_MIGRATOR")); +// migratorRouter = MigratorRouter( +// addresses.mainnet("V1_MIGRATION_ROUTER") +// ); +// usdcpsm = PegStabilityModule(addresses.mainnet("PSM_USDC")); +// daipsm = PegStabilityModule(addresses.mainnet("PSM_DAI")); +// dai = IERC20(addresses.mainnet("DAI")); +// usdc = IERC20(addresses.mainnet("USDC")); +// vso = VoltSystemOracle(addresses.mainnet("VOLT_SYSTEM_ORACLE")); +// grlm = addresses.mainnet("GLOBAL_RATE_LIMITED_MINTER"); +// multisig = addresses.mainnet("GOVERNOR"); +// coreV1 = addresses.mainnet("V1_CORE"); +// } + +// function testExchangeTo(uint64 amountOldVoltToExchange) public { +// oldVolt.approve(address(voltMigrator), type(uint256).max); +// deal(address(oldVolt), address(this), amountOldVoltToExchange); +// deal(address(volt), address(voltMigrator), amountOldVoltToExchange); + +// uint256 newVoltTotalSupply = volt.totalSupply(); +// uint256 oldVoltTotalSupply = oldVolt.totalSupply(); + +// uint256 oldVoltBalanceBefore = oldVolt.balanceOf(address(this)); +// uint256 newVoltBalanceBefore = volt.balanceOf(address(0xFFF)); + +// voltMigrator.exchangeTo(address(0xFFF), amountOldVoltToExchange); + +// uint256 oldVoltBalanceAfter = oldVolt.balanceOf(address(this)); +// uint256 newVoltBalanceAfter = volt.balanceOf(address(0xFFF)); + +// assertEq( +// newVoltBalanceAfter, +// newVoltBalanceBefore + amountOldVoltToExchange +// ); +// assertEq( +// oldVoltBalanceAfter, +// oldVoltBalanceBefore - amountOldVoltToExchange +// ); +// assertEq( +// oldVolt.totalSupply(), +// oldVoltTotalSupply - amountOldVoltToExchange +// ); +// assertEq(volt.totalSupply(), newVoltTotalSupply); /// new volt supply remains unchanged +// } + +// function testExchangeAllTo() public { +// uint256 amountOldVoltToExchange = 10_000e18; + +// oldVolt.approve(address(voltMigrator), type(uint256).max); + +// deal(address(oldVolt), address(this), amountOldVoltToExchange); +// deal(address(volt), address(voltMigrator), amountOldVoltToExchange); + +// uint256 newVoltTotalSupply = volt.totalSupply(); +// uint256 oldVoltTotalSupply = oldVolt.totalSupply(); + +// uint256 oldVoltBalanceBefore = oldVolt.balanceOf(address(this)); +// uint256 newVoltBalanceBefore = volt.balanceOf(address(0xFFF)); + +// voltMigrator.exchangeAllTo(address(0xFFF)); + +// uint256 oldVoltBalanceAfter = oldVolt.balanceOf(address(this)); +// uint256 newVoltBalanceAfter = volt.balanceOf(address(0xFFF)); + +// assertEq( +// newVoltBalanceAfter, +// newVoltBalanceBefore + oldVoltBalanceBefore +// ); +// assertEq(oldVoltBalanceAfter, 0); +// assertEq( +// oldVolt.totalSupply(), +// oldVoltTotalSupply - oldVoltBalanceBefore +// ); +// assertEq(volt.totalSupply(), newVoltTotalSupply); +// } + +// function testExchangeFailsWhenApprovalNotGiven() public { +// vm.expectRevert("ERC20: burn amount exceeds allowance"); +// voltMigrator.exchange(1e18); +// } + +// function testExchangeToFailsWhenApprovalNotGiven() public { +// vm.expectRevert("ERC20: burn amount exceeds allowance"); +// voltMigrator.exchangeTo(address(0xFFF), 1e18); +// } + +// function testExchangeFailsMigratorUnderfunded() public { +// uint256 amountOldVoltToExchange = 100_000_000e18; - vm.prank(grlm); - volt.mint(address(voltMigrator), amountOldVoltToExchange); - deal(address(oldVolt), address(this), amountOldVoltToExchange); +// vm.prank(grlm); +// volt.mint(address(voltMigrator), amountOldVoltToExchange); +// deal(address(oldVolt), address(this), amountOldVoltToExchange); - oldVolt.approve(address(voltMigrator), type(uint256).max); +// oldVolt.approve(address(voltMigrator), type(uint256).max); - vm.prank(address(voltMigrator)); - volt.burn(mintAmount); +// vm.prank(address(voltMigrator)); +// volt.burn(mintAmount); - vm.expectRevert(stdError.arithmeticError); - voltMigrator.exchange(amountOldVoltToExchange); - } +// vm.expectRevert(stdError.arithmeticError); +// voltMigrator.exchange(amountOldVoltToExchange); +// } - function testExchangeAllFailsMigratorUnderfunded() public { - oldVolt.approve(address(voltMigrator), type(uint256).max); - deal(address(oldVolt), address(this), mintAmount); +// function testExchangeAllFailsMigratorUnderfunded() public { +// oldVolt.approve(address(voltMigrator), type(uint256).max); +// deal(address(oldVolt), address(this), mintAmount); - vm.prank(grlm); - volt.mint(address(voltMigrator), mintAmount); +// vm.prank(grlm); +// volt.mint(address(voltMigrator), mintAmount); - vm.prank(address(voltMigrator)); - volt.burn(mintAmount); +// vm.prank(address(voltMigrator)); +// volt.burn(mintAmount); - vm.expectRevert(stdError.arithmeticError); - voltMigrator.exchangeAll(); - } +// vm.expectRevert(stdError.arithmeticError); +// voltMigrator.exchangeAll(); +// } - function testExchangeToFailsMigratorUnderfunded() public { - deal(address(oldVolt), address(this), mintAmount); +// function testExchangeToFailsMigratorUnderfunded() public { +// deal(address(oldVolt), address(this), mintAmount); - vm.prank(grlm); - volt.mint(address(voltMigrator), mintAmount); +// vm.prank(grlm); +// volt.mint(address(voltMigrator), mintAmount); - uint256 amountOldVoltToExchange = 100_000_000e18; - oldVolt.approve(address(voltMigrator), type(uint256).max); +// uint256 amountOldVoltToExchange = 100_000_000e18; +// oldVolt.approve(address(voltMigrator), type(uint256).max); - vm.prank(address(voltMigrator)); - volt.burn(mintAmount); +// vm.prank(address(voltMigrator)); +// volt.burn(mintAmount); - vm.expectRevert(stdError.arithmeticError); - voltMigrator.exchangeTo(address(0xFFF), amountOldVoltToExchange); - } +// vm.expectRevert(stdError.arithmeticError); +// voltMigrator.exchangeTo(address(0xFFF), amountOldVoltToExchange); +// } - function testExchangeAllToFailsMigratorUnderfunded() public { - vm.prank(grlm); - volt.mint(address(voltMigrator), mintAmount); - deal(address(oldVolt), address(this), mintAmount); +// function testExchangeAllToFailsMigratorUnderfunded() public { +// vm.prank(grlm); +// volt.mint(address(voltMigrator), mintAmount); +// deal(address(oldVolt), address(this), mintAmount); - oldVolt.approve(address(voltMigrator), type(uint256).max); +// oldVolt.approve(address(voltMigrator), type(uint256).max); - vm.prank(address(voltMigrator)); - volt.burn(mintAmount); +// vm.prank(address(voltMigrator)); +// volt.burn(mintAmount); - vm.expectRevert(stdError.arithmeticError); - voltMigrator.exchangeAllTo(address(0xFFF)); - } +// vm.expectRevert(stdError.arithmeticError); +// voltMigrator.exchangeAllTo(address(0xFFF)); +// } - function testExchangeAllWhenApprovalNotGiven() public { - uint256 oldVoltBalanceBefore = oldVolt.balanceOf(address(this)); - uint256 newVoltBalanceBefore = volt.balanceOf(address(this)); +// function testExchangeAllWhenApprovalNotGiven() public { +// uint256 oldVoltBalanceBefore = oldVolt.balanceOf(address(this)); +// uint256 newVoltBalanceBefore = volt.balanceOf(address(this)); - vm.expectRevert("VoltMigrator: no amount to exchange"); - voltMigrator.exchangeAll(); +// vm.expectRevert("VoltMigrator: no amount to exchange"); +// voltMigrator.exchangeAll(); - uint256 oldVoltBalanceAfter = oldVolt.balanceOf(address(this)); - uint256 newVoltBalanceAfter = volt.balanceOf(address(this)); +// uint256 oldVoltBalanceAfter = oldVolt.balanceOf(address(this)); +// uint256 newVoltBalanceAfter = volt.balanceOf(address(this)); - assertEq(oldVoltBalanceBefore, oldVoltBalanceAfter); - assertEq(newVoltBalanceBefore, newVoltBalanceAfter); - } +// assertEq(oldVoltBalanceBefore, oldVoltBalanceAfter); +// assertEq(newVoltBalanceBefore, newVoltBalanceAfter); +// } - function testExchangeAllToWhenApprovalNotGiven() public { - uint256 oldVoltBalanceBefore = oldVolt.balanceOf(address(this)); - uint256 newVoltBalanceBefore = volt.balanceOf(address(0xFFF)); +// function testExchangeAllToWhenApprovalNotGiven() public { +// uint256 oldVoltBalanceBefore = oldVolt.balanceOf(address(this)); +// uint256 newVoltBalanceBefore = volt.balanceOf(address(0xFFF)); - vm.expectRevert("VoltMigrator: no amount to exchange"); - voltMigrator.exchangeAllTo(address(0xFFF)); +// vm.expectRevert("VoltMigrator: no amount to exchange"); +// voltMigrator.exchangeAllTo(address(0xFFF)); - uint256 oldVoltBalanceAfter = oldVolt.balanceOf(address(this)); - uint256 newVoltBalanceAfter = volt.balanceOf(address(0xFFF)); +// uint256 oldVoltBalanceAfter = oldVolt.balanceOf(address(this)); +// uint256 newVoltBalanceAfter = volt.balanceOf(address(0xFFF)); - assertEq(oldVoltBalanceBefore, oldVoltBalanceAfter); - assertEq(newVoltBalanceBefore, newVoltBalanceAfter); - } +// assertEq(oldVoltBalanceBefore, oldVoltBalanceAfter); +// assertEq(newVoltBalanceBefore, newVoltBalanceAfter); +// } - function testExchangeAllPartialApproval() public { - deal(address(oldVolt), address(this), 100_000e18); +// function testExchangeAllPartialApproval() public { +// deal(address(oldVolt), address(this), 100_000e18); - uint256 amountOldVoltToExchange = oldVolt.balanceOf(address(this)) / 2; // exchange half of users balance +// uint256 amountOldVoltToExchange = oldVolt.balanceOf(address(this)) / 2; // exchange half of users balance - oldVolt.approve(address(voltMigrator), amountOldVoltToExchange); +// oldVolt.approve(address(voltMigrator), amountOldVoltToExchange); - vm.prank(grlm); - volt.mint(address(voltMigrator), amountOldVoltToExchange); +// vm.prank(grlm); +// volt.mint(address(voltMigrator), amountOldVoltToExchange); - uint256 newVoltTotalSupply = volt.totalSupply(); - uint256 oldVoltTotalSupply = oldVolt.totalSupply(); +// uint256 newVoltTotalSupply = volt.totalSupply(); +// uint256 oldVoltTotalSupply = oldVolt.totalSupply(); - uint256 oldVoltBalanceBefore = oldVolt.balanceOf(address(this)); - uint256 newVoltBalanceBefore = volt.balanceOf(address(this)); +// uint256 oldVoltBalanceBefore = oldVolt.balanceOf(address(this)); +// uint256 newVoltBalanceBefore = volt.balanceOf(address(this)); - voltMigrator.exchangeAllTo(address(this)); +// voltMigrator.exchangeAllTo(address(this)); - uint256 oldVoltBalanceAfter = oldVolt.balanceOf(address(this)); - uint256 newVoltBalanceAfter = volt.balanceOf(address(this)); +// uint256 oldVoltBalanceAfter = oldVolt.balanceOf(address(this)); +// uint256 newVoltBalanceAfter = volt.balanceOf(address(this)); - assertEq( - newVoltBalanceAfter, - newVoltBalanceBefore + amountOldVoltToExchange - ); - assertEq( - oldVoltBalanceAfter, - oldVoltBalanceBefore - amountOldVoltToExchange - ); +// assertEq( +// newVoltBalanceAfter, +// newVoltBalanceBefore + amountOldVoltToExchange +// ); +// assertEq( +// oldVoltBalanceAfter, +// oldVoltBalanceBefore - amountOldVoltToExchange +// ); - assertEq( - oldVolt.totalSupply(), - oldVoltTotalSupply - amountOldVoltToExchange - ); - assertEq(volt.totalSupply(), newVoltTotalSupply); +// assertEq( +// oldVolt.totalSupply(), +// oldVoltTotalSupply - amountOldVoltToExchange +// ); +// assertEq(volt.totalSupply(), newVoltTotalSupply); - assertEq(oldVoltBalanceAfter, oldVoltBalanceBefore / 2); - } +// assertEq(oldVoltBalanceAfter, oldVoltBalanceBefore / 2); +// } - function testExchangeAllToPartialApproval() public { - uint256 amountOldVoltToExchange = mintAmount / 2; // exchange half of users balance - oldVolt.approve(address(voltMigrator), amountOldVoltToExchange); +// function testExchangeAllToPartialApproval() public { +// uint256 amountOldVoltToExchange = mintAmount / 2; // exchange half of users balance +// oldVolt.approve(address(voltMigrator), amountOldVoltToExchange); - vm.prank(grlm); - volt.mint(address(voltMigrator), mintAmount); +// vm.prank(grlm); +// volt.mint(address(voltMigrator), mintAmount); - vm.prank(multisig); - CoreV2(coreV1).grantMinter(address(this)); - oldVolt.mint(address(this), mintAmount); +// vm.prank(multisig); +// CoreV2(coreV1).grantMinter(address(this)); +// oldVolt.mint(address(this), mintAmount); - uint256 newVoltTotalSupply = volt.totalSupply(); - uint256 oldVoltTotalSupply = oldVolt.totalSupply(); +// uint256 newVoltTotalSupply = volt.totalSupply(); +// uint256 oldVoltTotalSupply = oldVolt.totalSupply(); - uint256 oldVoltBalanceBefore = oldVolt.balanceOf(address(this)); - uint256 newVoltBalanceBefore = volt.balanceOf(address(0xFFF)); +// uint256 oldVoltBalanceBefore = oldVolt.balanceOf(address(this)); +// uint256 newVoltBalanceBefore = volt.balanceOf(address(0xFFF)); - voltMigrator.exchangeAllTo(address(0xFFF)); +// voltMigrator.exchangeAllTo(address(0xFFF)); - uint256 oldVoltBalanceAfter = oldVolt.balanceOf(address(this)); - uint256 newVoltBalanceAfter = volt.balanceOf(address(0xFFF)); +// uint256 oldVoltBalanceAfter = oldVolt.balanceOf(address(this)); +// uint256 newVoltBalanceAfter = volt.balanceOf(address(0xFFF)); - assertEq( - newVoltBalanceAfter, - newVoltBalanceBefore + amountOldVoltToExchange - ); - assertEq( - oldVoltBalanceAfter, - oldVoltBalanceBefore - amountOldVoltToExchange - ); - assertEq( - oldVolt.totalSupply(), - oldVoltTotalSupply - amountOldVoltToExchange - ); - assertEq(volt.totalSupply(), newVoltTotalSupply); - assertEq(oldVoltBalanceAfter, oldVoltBalanceBefore / 2); - } +// assertEq( +// newVoltBalanceAfter, +// newVoltBalanceBefore + amountOldVoltToExchange +// ); +// assertEq( +// oldVoltBalanceAfter, +// oldVoltBalanceBefore - amountOldVoltToExchange +// ); +// assertEq( +// oldVolt.totalSupply(), +// oldVoltTotalSupply - amountOldVoltToExchange +// ); +// assertEq(volt.totalSupply(), newVoltTotalSupply); +// assertEq(oldVoltBalanceAfter, oldVoltBalanceBefore / 2); +// } - function testSweep() public { - uint256 amountToTransfer = 1_000_000e6; +// function testSweep() public { +// uint256 amountToTransfer = 1_000_000e6; - uint256 startingBalance = usdc.balanceOf(address(timelockController)); +// uint256 startingBalance = usdc.balanceOf(address(timelockController)); - deal(address(usdc), address(voltMigrator), amountToTransfer); +// deal(address(usdc), address(voltMigrator), amountToTransfer); - vm.prank(multisig); - voltMigrator.sweep( - address(usdc), - address(timelockController), - amountToTransfer - ); +// vm.prank(multisig); +// voltMigrator.sweep( +// address(usdc), +// address(timelockController), +// amountToTransfer +// ); - uint256 endingBalance = usdc.balanceOf(address(timelockController)); +// uint256 endingBalance = usdc.balanceOf(address(timelockController)); - assertEq(endingBalance - startingBalance, amountToTransfer); - } +// assertEq(endingBalance - startingBalance, amountToTransfer); +// } - function testSweepNonGovernorFails() public { - uint256 amountToTransfer = 1_000_000e6; +// function testSweepNonGovernorFails() public { +// uint256 amountToTransfer = 1_000_000e6; - deal(address(usdc), address(voltMigrator), amountToTransfer); +// deal(address(usdc), address(voltMigrator), amountToTransfer); - vm.expectRevert("CoreRef: Caller is not a governor"); - voltMigrator.sweep( - address(usdc), - address(timelockController), - amountToTransfer - ); - } +// vm.expectRevert("CoreRef: Caller is not a governor"); +// voltMigrator.sweep( +// address(usdc), +// address(timelockController), +// amountToTransfer +// ); +// } - function testSweepNewVoltFails() public { - uint256 amountToSweep = volt.balanceOf(address(voltMigrator)); +// function testSweepNewVoltFails() public { +// uint256 amountToSweep = volt.balanceOf(address(voltMigrator)); - vm.prank(multisig); - vm.expectRevert("VoltMigrator: cannot sweep new Volt"); - voltMigrator.sweep( - address(volt), - address(timelockController), - amountToSweep - ); - } +// vm.prank(multisig); +// vm.expectRevert("VoltMigrator: cannot sweep new Volt"); +// voltMigrator.sweep( +// address(volt), +// address(timelockController), +// amountToSweep +// ); +// } - function testRedeemUsdc(uint72 amountVoltIn) public { - oldVolt.approve(address(migratorRouter), type(uint256).max); +// function testRedeemUsdc(uint72 amountVoltIn) public { +// oldVolt.approve(address(migratorRouter), type(uint256).max); - vm.prank(grlm); - volt.mint(address(voltMigrator), amountVoltIn); +// vm.prank(grlm); +// volt.mint(address(voltMigrator), amountVoltIn); - vm.prank(multisig); - CoreV2(coreV1).grantMinter(address(this)); - oldVolt.mint(address(this), amountVoltIn); +// vm.prank(multisig); +// CoreV2(coreV1).grantMinter(address(this)); +// oldVolt.mint(address(this), amountVoltIn); - uint256 startBalance = usdc.balanceOf(address(this)); - uint256 minAmountOut = usdcpsm.getRedeemAmountOut(amountVoltIn); +// uint256 startBalance = usdc.balanceOf(address(this)); +// uint256 minAmountOut = usdcpsm.getRedeemAmountOut(amountVoltIn); - deal(address(usdc), address(usdcpsm), minAmountOut); +// deal(address(usdc), address(usdcpsm), minAmountOut); - uint256 currentPegPrice = vso.getCurrentOraclePrice() / 1e12; - uint256 amountOut = (amountVoltIn * currentPegPrice) / 1e18; +// uint256 currentPegPrice = vso.getCurrentOraclePrice() / 1e12; +// uint256 amountOut = (amountVoltIn * currentPegPrice) / 1e18; - uint256 redeemedAmount = migratorRouter.redeemUSDC( - amountVoltIn, - minAmountOut - ); - uint256 endBalance = usdc.balanceOf(address(this)); +// uint256 redeemedAmount = migratorRouter.redeemUSDC( +// amountVoltIn, +// minAmountOut +// ); +// uint256 endBalance = usdc.balanceOf(address(this)); - assertApproxEq(minAmountOut.toInt256(), amountOut.toInt256(), 0); - assertEq(minAmountOut, endBalance - startBalance); - assertEq(redeemedAmount, minAmountOut); - } +// assertApproxEq(minAmountOut.toInt256(), amountOut.toInt256(), 0); +// assertEq(minAmountOut, endBalance - startBalance); +// assertEq(redeemedAmount, minAmountOut); +// } - function testRedeemDai(uint72 amountVoltIn) public { - oldVolt.approve(address(migratorRouter), type(uint256).max); +// function testRedeemDai(uint72 amountVoltIn) public { +// oldVolt.approve(address(migratorRouter), type(uint256).max); - vm.prank(grlm); - volt.mint(address(voltMigrator), amountVoltIn); +// vm.prank(grlm); +// volt.mint(address(voltMigrator), amountVoltIn); - vm.prank(multisig); - CoreV2(coreV1).grantMinter(address(this)); - oldVolt.mint(address(this), amountVoltIn); +// vm.prank(multisig); +// CoreV2(coreV1).grantMinter(address(this)); +// oldVolt.mint(address(this), amountVoltIn); - uint256 startBalance = dai.balanceOf(address(this)); - uint256 minAmountOut = daipsm.getRedeemAmountOut(amountVoltIn); +// uint256 startBalance = dai.balanceOf(address(this)); +// uint256 minAmountOut = daipsm.getRedeemAmountOut(amountVoltIn); - deal(address(dai), address(daipsm), minAmountOut); +// deal(address(dai), address(daipsm), minAmountOut); - uint256 currentPegPrice = vso.getCurrentOraclePrice(); - uint256 amountOut = (amountVoltIn * currentPegPrice) / 1e18; +// uint256 currentPegPrice = vso.getCurrentOraclePrice(); +// uint256 amountOut = (amountVoltIn * currentPegPrice) / 1e18; - uint256 redeemedAmount = migratorRouter.redeemDai( - amountVoltIn, - minAmountOut - ); - uint256 endBalance = dai.balanceOf(address(this)); +// uint256 redeemedAmount = migratorRouter.redeemDai( +// amountVoltIn, +// minAmountOut +// ); +// uint256 endBalance = dai.balanceOf(address(this)); - assertApproxEq(minAmountOut.toInt256(), amountOut.toInt256(), 0); - assertEq(minAmountOut, endBalance - startBalance); - assertEq(redeemedAmount, minAmountOut); - } +// assertApproxEq(minAmountOut.toInt256(), amountOut.toInt256(), 0); +// assertEq(minAmountOut, endBalance - startBalance); +// assertEq(redeemedAmount, minAmountOut); +// } - function testRedeemDaiFailsUserNotEnoughVolt() public { - oldVolt.approve(address(migratorRouter), type(uint256).max); +// function testRedeemDaiFailsUserNotEnoughVolt() public { +// oldVolt.approve(address(migratorRouter), type(uint256).max); - uint256 minAmountOut = daipsm.getRedeemAmountOut(mintAmount); +// uint256 minAmountOut = daipsm.getRedeemAmountOut(mintAmount); - vm.expectRevert("ERC20: transfer amount exceeds balance"); - migratorRouter.redeemDai(mintAmount, minAmountOut); - } +// vm.expectRevert("ERC20: transfer amount exceeds balance"); +// migratorRouter.redeemDai(mintAmount, minAmountOut); +// } - function testRedeemUsdcFailsUserNotEnoughVolt() public { - oldVolt.approve(address(migratorRouter), type(uint256).max); +// function testRedeemUsdcFailsUserNotEnoughVolt() public { +// oldVolt.approve(address(migratorRouter), type(uint256).max); - uint256 minAmountOut = daipsm.getRedeemAmountOut(mintAmount); +// uint256 minAmountOut = daipsm.getRedeemAmountOut(mintAmount); - vm.expectRevert("ERC20: transfer amount exceeds balance"); - migratorRouter.redeemUSDC(mintAmount, minAmountOut); - } +// vm.expectRevert("ERC20: transfer amount exceeds balance"); +// migratorRouter.redeemUSDC(mintAmount, minAmountOut); +// } - function testRedeemDaiFailUnderfundedPSM() public { - oldVolt.approve(address(migratorRouter), type(uint256).max); +// function testRedeemDaiFailUnderfundedPSM() public { +// oldVolt.approve(address(migratorRouter), type(uint256).max); - vm.prank(grlm); - volt.mint(address(voltMigrator), mintAmount); +// vm.prank(grlm); +// volt.mint(address(voltMigrator), mintAmount); - vm.prank(multisig); - CoreV2(coreV1).grantMinter(address(this)); - oldVolt.mint(address(this), mintAmount); +// vm.prank(multisig); +// CoreV2(coreV1).grantMinter(address(this)); +// oldVolt.mint(address(this), mintAmount); - uint256 balance = dai.balanceOf(address(daipsm)); - vm.prank(address(daipsm)); - dai.transfer(address(0), balance); +// uint256 balance = dai.balanceOf(address(daipsm)); +// vm.prank(address(daipsm)); +// dai.transfer(address(0), balance); - uint256 minAmountOut = daipsm.getRedeemAmountOut(mintAmount); +// uint256 minAmountOut = daipsm.getRedeemAmountOut(mintAmount); - vm.expectRevert("Dai/insufficient-balance"); - migratorRouter.redeemDai(mintAmount, minAmountOut); - } +// vm.expectRevert("Dai/insufficient-balance"); +// migratorRouter.redeemDai(mintAmount, minAmountOut); +// } - function testRedeemUsdcFailUnderfundedPSM() public { - oldVolt.approve(address(migratorRouter), type(uint256).max); +// function testRedeemUsdcFailUnderfundedPSM() public { +// oldVolt.approve(address(migratorRouter), type(uint256).max); - vm.prank(grlm); - volt.mint(address(voltMigrator), mintAmount); +// vm.prank(grlm); +// volt.mint(address(voltMigrator), mintAmount); - vm.prank(multisig); - CoreV2(coreV1).grantMinter(address(this)); - oldVolt.mint(address(this), mintAmount); +// vm.prank(multisig); +// CoreV2(coreV1).grantMinter(address(this)); +// oldVolt.mint(address(this), mintAmount); - uint256 balance = usdc.balanceOf(address(usdcpsm)); - vm.prank(address(usdcpsm)); - usdc.transfer(address(1), balance); +// uint256 balance = usdc.balanceOf(address(usdcpsm)); +// vm.prank(address(usdcpsm)); +// usdc.transfer(address(1), balance); - uint256 minAmountOut = usdcpsm.getRedeemAmountOut(mintAmount); +// uint256 minAmountOut = usdcpsm.getRedeemAmountOut(mintAmount); - vm.expectRevert("ERC20: transfer amount exceeds balance"); - migratorRouter.redeemUSDC(mintAmount, minAmountOut); - } +// vm.expectRevert("ERC20: transfer amount exceeds balance"); +// migratorRouter.redeemUSDC(mintAmount, minAmountOut); +// } - function testRedeemDaiFailNoUserApproval() public { - vm.prank(multisig); - CoreV2(coreV1).grantMinter(address(this)); - oldVolt.mint(address(this), mintAmount); +// function testRedeemDaiFailNoUserApproval() public { +// vm.prank(multisig); +// CoreV2(coreV1).grantMinter(address(this)); +// oldVolt.mint(address(this), mintAmount); - vm.prank(grlm); - volt.mint(address(voltMigrator), mintAmount); +// vm.prank(grlm); +// volt.mint(address(voltMigrator), mintAmount); - uint256 minAmountOut = daipsm.getRedeemAmountOut(mintAmount); +// uint256 minAmountOut = daipsm.getRedeemAmountOut(mintAmount); - vm.expectRevert("ERC20: transfer amount exceeds allowance"); - migratorRouter.redeemDai(mintAmount, minAmountOut); - } +// vm.expectRevert("ERC20: transfer amount exceeds allowance"); +// migratorRouter.redeemDai(mintAmount, minAmountOut); +// } - function testRedeemUsdcFailNoUserApproval() public { - vm.prank(multisig); - CoreV2(coreV1).grantMinter(address(this)); - oldVolt.mint(address(this), mintAmount); +// function testRedeemUsdcFailNoUserApproval() public { +// vm.prank(multisig); +// CoreV2(coreV1).grantMinter(address(this)); +// oldVolt.mint(address(this), mintAmount); - vm.prank(grlm); - volt.mint(address(voltMigrator), mintAmount); +// vm.prank(grlm); +// volt.mint(address(voltMigrator), mintAmount); - uint256 minAmountOut = daipsm.getRedeemAmountOut(mintAmount); +// uint256 minAmountOut = daipsm.getRedeemAmountOut(mintAmount); - vm.expectRevert("ERC20: transfer amount exceeds allowance"); - migratorRouter.redeemUSDC(mintAmount, minAmountOut); - } +// vm.expectRevert("ERC20: transfer amount exceeds allowance"); +// migratorRouter.redeemUSDC(mintAmount, minAmountOut); +// } - function testRedeemDaiFailUnderfundedMigrator() public { - oldVolt.approve(address(migratorRouter), type(uint256).max); +// function testRedeemDaiFailUnderfundedMigrator() public { +// oldVolt.approve(address(migratorRouter), type(uint256).max); - vm.prank(multisig); - CoreV2(coreV1).grantMinter(address(this)); - oldVolt.mint(address(this), mintAmount); +// vm.prank(multisig); +// CoreV2(coreV1).grantMinter(address(this)); +// oldVolt.mint(address(this), mintAmount); - uint256 minAmountOut = usdcpsm.getRedeemAmountOut(mintAmount); +// uint256 minAmountOut = usdcpsm.getRedeemAmountOut(mintAmount); - vm.expectRevert(stdError.arithmeticError); - migratorRouter.redeemDai(mintAmount, minAmountOut); - } +// vm.expectRevert(stdError.arithmeticError); +// migratorRouter.redeemDai(mintAmount, minAmountOut); +// } - function testRedeemUsdcFailUnderfundedMigrator() public { - oldVolt.approve(address(migratorRouter), type(uint256).max); +// function testRedeemUsdcFailUnderfundedMigrator() public { +// oldVolt.approve(address(migratorRouter), type(uint256).max); - vm.prank(multisig); - CoreV2(coreV1).grantMinter(address(this)); - oldVolt.mint(address(this), mintAmount); +// vm.prank(multisig); +// CoreV2(coreV1).grantMinter(address(this)); +// oldVolt.mint(address(this), mintAmount); - uint256 minAmountOut = usdcpsm.getRedeemAmountOut(mintAmount); +// uint256 minAmountOut = usdcpsm.getRedeemAmountOut(mintAmount); - vm.expectRevert(stdError.arithmeticError); - migratorRouter.redeemUSDC(mintAmount, minAmountOut); - } -} +// vm.expectRevert(stdError.arithmeticError); +// migratorRouter.redeemUSDC(mintAmount, minAmountOut); +// } +// } diff --git a/test/integration/proposal-checks/IntegrationTestProposalPSMOracle.sol b/test/integration/proposal-checks/IntegrationTestProposalPSMOracle.sol index d8a79856d..eef9e25d4 100644 --- a/test/integration/proposal-checks/IntegrationTestProposalPSMOracle.sol +++ b/test/integration/proposal-checks/IntegrationTestProposalPSMOracle.sol @@ -8,7 +8,7 @@ import {Addresses} from "@test/proposals/Addresses.sol"; import {IOracleRef} from "@voltprotocol/v1/IOracleRef.sol"; import {IOracleRefV2} from "@voltprotocol/refs/IOracleRefV2.sol"; import {TestProposals} from "@test/proposals/TestProposals.sol"; -import {PegStabilityModule} from "@voltprotocol/peg/PegStabilityModule.sol"; +import {IPegStabilityModule} from "@voltprotocol/peg/IPegStabilityModule.sol"; contract IntegrationTestProposalPSMOracle is Test { function setUp() public {} @@ -77,7 +77,7 @@ contract IntegrationTestProposalPSMOracle is Test { function testPSMSameMint() public { // Init Addresses addresses = new Addresses(); - PegStabilityModule psm = PegStabilityModule( + IPegStabilityModule psm = IPegStabilityModule( addresses.mainnet("PSM_USDC") ); IERC20 volt = IERC20(addresses.mainnet("VOLT")); @@ -99,7 +99,7 @@ contract IntegrationTestProposalPSMOracle is Test { addresses = proposals.addresses(); // get post-proposal addresses // Use post-proposal contracts if they have been migrated - psm = PegStabilityModule(addresses.mainnet("PSM_USDC")); + psm = IPegStabilityModule(addresses.mainnet("PSM_USDC")); volt = IERC20(addresses.mainnet("VOLT")); token = IERC20(addresses.mainnet("USDC")); diff --git a/test/mock/MockRateLimitedV2.sol b/test/mock/MockRateLimitedV2.sol index 0b2b47462..9fb8e48f1 100644 --- a/test/mock/MockRateLimitedV2.sol +++ b/test/mock/MockRateLimitedV2.sol @@ -7,8 +7,8 @@ contract MockRateLimitedV2 is RateLimitedV2 { constructor( address _core, uint256 _maxRateLimitPerSecond, - uint128 _rateLimitPerSecond, - uint128 _bufferCap + uint64 _rateLimitPerSecond, + uint96 _bufferCap ) RateLimitedV2(_maxRateLimitPerSecond, _rateLimitPerSecond, _bufferCap) CoreRefV2(_core) diff --git a/test/proposals/vips/vip15.sol b/test/proposals/vips/vip15.sol index 945e8e048..146ab5934 100644 --- a/test/proposals/vips/vip15.sol +++ b/test/proposals/vips/vip15.sol @@ -1,16 +1,16 @@ //SPDX-License-Identifier: GPL-3.0-or-later pragma solidity =0.8.13; -import {Addresses} from "@test/proposals/Addresses.sol"; -import {TimelockProposal} from "@test/proposals/proposalTypes/TimelockProposal.sol"; - import {TimelockController} from "@openzeppelin/contracts/governance/TimelockController.sol"; import {Core} from "@voltprotocol/v1/Core.sol"; +import {Addresses} from "@test/proposals/Addresses.sol"; +import {CoreRefV2} from "@voltprotocol/refs/CoreRefV2.sol"; import {VoltRoles} from "@voltprotocol/core/VoltRoles.sol"; import {PCVDeposit} from "@voltprotocol/pcv/PCVDeposit.sol"; -import {ERC20Allocator} from "@voltprotocol/pcv/ERC20Allocator.sol"; -import {PegStabilityModule} from "@voltprotocol/peg/PegStabilityModule.sol"; +import {IERC20Allocator} from "@voltprotocol/pcv/IERC20Allocator.sol"; +import {NonCustodialPSM} from "@voltprotocol/peg/NonCustodialPSM.sol"; +import {TimelockProposal} from "@test/proposals/proposalTypes/TimelockProposal.sol"; /* VIP15 deprecates the old system and sends all protocol funds to the @@ -354,13 +354,13 @@ contract vip15 is TimelockProposal { function validate(Addresses addresses, address /* deployer*/) public { Core core = Core(addresses.mainnet("CORE")); - PegStabilityModule daiPriceBoundPSM = PegStabilityModule( + NonCustodialPSM daiPriceBoundPSM = NonCustodialPSM( addresses.mainnet("VOLT_DAI_PSM") ); - PegStabilityModule usdcPriceBoundPSM = PegStabilityModule( + NonCustodialPSM usdcPriceBoundPSM = NonCustodialPSM( addresses.mainnet("VOLT_USDC_PSM") ); - ERC20Allocator allocator = ERC20Allocator( + IERC20Allocator allocator = IERC20Allocator( addresses.mainnet("ERC20ALLOCATOR") ); PCVDeposit daiDeposit = PCVDeposit( @@ -416,7 +416,7 @@ contract vip15 is TimelockProposal { assertTrue(daiDeposit.paused()); assertTrue(usdcDeposit.paused()); - assertTrue(allocator.paused()); + assertTrue(CoreRefV2(address(allocator)).paused()); /// pcv deposits assertTrue(daiDeposit.balance() < 1e18); /// less than $1 left in Morpho diff --git a/test/proposals/vips/vip16.sol b/test/proposals/vips/vip16.sol index c10df748e..6146b6fe7 100644 --- a/test/proposals/vips/vip16.sol +++ b/test/proposals/vips/vip16.sol @@ -19,23 +19,21 @@ import {IPCVOracle} from "@voltprotocol/oracle/IPCVOracle.sol"; import {PCVDeposit} from "@voltprotocol/pcv/PCVDeposit.sol"; import {SystemEntry} from "@voltprotocol/entry/SystemEntry.sol"; import {PCVGuardian} from "@voltprotocol/pcv/PCVGuardian.sol"; +import {IOracleRefV2} from "@voltprotocol/refs/IOracleRefV2.sol"; import {IPCVDepositV2} from "@voltprotocol/pcv/IPCVDepositV2.sol"; import {MigratorRouter} from "@voltprotocol/v1-migration/MigratorRouter.sol"; -import {ERC20Allocator} from "@voltprotocol/pcv/ERC20Allocator.sol"; import {NonCustodialPSM} from "@voltprotocol/peg/NonCustodialPSM.sol"; import {MakerPCVSwapper} from "@voltprotocol/pcv/maker/MakerPCVSwapper.sol"; import {VoltSystemOracle} from "@voltprotocol/oracle/VoltSystemOracle.sol"; import {EulerPCVDeposit} from "@voltprotocol/pcv/euler/EulerPCVDeposit.sol"; import {MorphoCompoundPCVDeposit} from "@voltprotocol/pcv/morpho/MorphoCompoundPCVDeposit.sol"; import {MorphoAavePCVDeposit} from "@voltprotocol/pcv/morpho/MorphoAavePCVDeposit.sol"; -import {PegStabilityModule} from "@voltprotocol/peg/PegStabilityModule.sol"; import {IPegStabilityModule} from "@voltprotocol/peg/IPegStabilityModule.sol"; import {ConstantPriceOracle} from "@voltprotocol/oracle/ConstantPriceOracle.sol"; import {CompoundBadDebtSentinel} from "@voltprotocol/pcv/compound/CompoundBadDebtSentinel.sol"; import {IVoltMigrator, VoltMigrator} from "@voltprotocol/v1-migration/VoltMigrator.sol"; import {IGlobalReentrancyLock, GlobalReentrancyLock} from "@voltprotocol/core/GlobalReentrancyLock.sol"; import {IGlobalRateLimitedMinter, GlobalRateLimitedMinter} from "@voltprotocol/rate-limits/GlobalRateLimitedMinter.sol"; -import {IGlobalSystemExitRateLimiter, GlobalSystemExitRateLimiter} from "@voltprotocol/rate-limits/GlobalSystemExitRateLimiter.sol"; /* VIP16 Executes after VIP15 (SystemV1 deprecation), and deploys the new SystemV2. @@ -68,10 +66,10 @@ contract vip16 is Proposal { uint256 public constant MAX_RATE_LIMIT_PER_SECOND_MINTING = 100e18; /// replenish 0 VOLT per day - uint128 public constant RATE_LIMIT_PER_SECOND_MINTING = 0; + uint64 public constant RATE_LIMIT_PER_SECOND_MINTING = 0; /// buffer cap of 3m VOLT - uint128 public constant BUFFER_CAP_MINTING = 3_000_000e18; + uint96 public constant BUFFER_CAP_MINTING = 3_000_000e18; /// ---------- RATE LIMITED MINTER PARAMS ---------- @@ -84,12 +82,6 @@ contract vip16 is Proposal { /// buffer cap of 0.5m VOLT uint128 public constant BUFFER_CAP_EXITING = 500_000e18; - /// ---------- ALLOCATOR PARAMS ---------- - - uint256 public constant ALLOCATOR_MAX_RATE_LIMIT_PER_SECOND = 1_000e18; /// 1k volt per second - uint128 public constant ALLOCATOR_RATE_LIMIT_PER_SECOND = 10e18; /// 10 volt per second - uint128 public constant ALLOCATOR_BUFFER_CAP = 1_000_000e18; /// buffer cap is 1m volt - /// ---------- PSM PARAMS ---------- uint128 public constant VOLT_FLOOR_PRICE_USDC = 1.05e6; @@ -132,22 +124,12 @@ contract vip16 is Proposal { RATE_LIMIT_PER_SECOND_MINTING, BUFFER_CAP_MINTING ); - GlobalSystemExitRateLimiter gserl = new GlobalSystemExitRateLimiter( - addresses.mainnet("CORE"), - MAX_RATE_LIMIT_PER_SECOND_EXITING, - RATE_LIMIT_PER_SECOND_EXIT, - BUFFER_CAP_EXITING - ); addresses.addMainnet( "TIMELOCK_CONTROLLER", address(timelockController) ); addresses.addMainnet("GLOBAL_RATE_LIMITED_MINTER", address(grlm)); - addresses.addMainnet( - "GLOBAL_SYSTEM_EXIT_RATE_LIMITER", - address(gserl) - ); } /// PCV Deposits @@ -248,26 +230,6 @@ contract vip16 is Proposal { /// Peg Stability { - PegStabilityModule daipsm = new PegStabilityModule( - addresses.mainnet("CORE"), - addresses.mainnet("VOLT_SYSTEM_ORACLE"), - address(0), - 0, - false, - IERC20(addresses.mainnet("DAI")), - VOLT_FLOOR_PRICE_DAI, - VOLT_CEILING_PRICE_DAI - ); - PegStabilityModule usdcpsm = new PegStabilityModule( - addresses.mainnet("CORE"), - addresses.mainnet("VOLT_SYSTEM_ORACLE"), - address(0), - -12, - false, - IERC20(addresses.mainnet("USDC")), - VOLT_FLOOR_PRICE_USDC, - VOLT_CEILING_PRICE_USDC - ); NonCustodialPSM usdcNonCustodialPsm = new NonCustodialPSM( addresses.mainnet("CORE"), addresses.mainnet("VOLT_SYSTEM_ORACLE"), @@ -294,12 +256,6 @@ contract vip16 is Proposal { addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_DAI") ) ); - ERC20Allocator allocator = new ERC20Allocator( - addresses.mainnet("CORE") - ); - - addresses.addMainnet("PSM_DAI", address(daipsm)); - addresses.addMainnet("PSM_USDC", address(usdcpsm)); addresses.addMainnet( "PSM_NONCUSTODIAL_USDC", address(usdcNonCustodialPsm) @@ -308,7 +264,6 @@ contract vip16 is Proposal { "PSM_NONCUSTODIAL_DAI", address(daiNonCustodialPsm) ); - addresses.addMainnet("PSM_ALLOCATOR", address(allocator)); } /// Volt Migration @@ -318,21 +273,22 @@ contract vip16 is Proposal { IVolt(addresses.mainnet("VOLT")) ); - MigratorRouter migratorRouter = new MigratorRouter( - IVolt(addresses.mainnet("VOLT")), - IVoltMigrator(address(voltMigrator)), - IPegStabilityModule(addresses.mainnet("PSM_DAI")), - IPegStabilityModule(addresses.mainnet("PSM_USDC")) - ); + /// TODO revisit this + // MigratorRouter migratorRouter = new MigratorRouter( + // IVolt(addresses.mainnet("VOLT")), + // IVoltMigrator(address(voltMigrator)), + // IPegStabilityModule(addresses.mainnet("PSM_DAI")), + // IPegStabilityModule(addresses.mainnet("PSM_USDC")) + // ); addresses.addMainnet( "V1_MIGRATION_MIGRATOR", address(voltMigrator) ); - addresses.addMainnet( - "V1_MIGRATION_ROUTER", - address(migratorRouter) - ); + // addresses.addMainnet( + // "V1_MIGRATION_ROUTER", + // address(migratorRouter) + // ); } /// PCV Movement @@ -345,25 +301,23 @@ contract vip16 is Proposal { addresses.mainnet("CORE") ); - address[] memory pcvGuardianSafeAddresses = new address[](8); + address[] memory pcvGuardianSafeAddresses = new address[](6); pcvGuardianSafeAddresses[0] = addresses.mainnet( "PCV_DEPOSIT_MORPHO_COMPOUND_DAI" ); pcvGuardianSafeAddresses[1] = addresses.mainnet( "PCV_DEPOSIT_MORPHO_COMPOUND_USDC" ); - pcvGuardianSafeAddresses[2] = addresses.mainnet("PSM_USDC"); - pcvGuardianSafeAddresses[3] = addresses.mainnet("PSM_DAI"); - pcvGuardianSafeAddresses[4] = addresses.mainnet( + pcvGuardianSafeAddresses[2] = addresses.mainnet( "PCV_DEPOSIT_EULER_DAI" ); - pcvGuardianSafeAddresses[5] = addresses.mainnet( + pcvGuardianSafeAddresses[3] = addresses.mainnet( "PCV_DEPOSIT_EULER_USDC" ); - pcvGuardianSafeAddresses[6] = addresses.mainnet( + pcvGuardianSafeAddresses[4] = addresses.mainnet( "PCV_DEPOSIT_MORPHO_AAVE_DAI" ); - pcvGuardianSafeAddresses[7] = addresses.mainnet( + pcvGuardianSafeAddresses[5] = addresses.mainnet( "PCV_DEPOSIT_MORPHO_AAVE_USDC" ); @@ -421,9 +375,6 @@ contract vip16 is Proposal { TimelockController timelockController = TimelockController( payable(addresses.mainnet("TIMELOCK_CONTROLLER")) ); - ERC20Allocator allocator = ERC20Allocator( - addresses.mainnet("PSM_ALLOCATOR") - ); PCVOracle pcvOracle = PCVOracle(addresses.mainnet("PCV_ORACLE")); PCVRouter pcvRouter = PCVRouter(addresses.mainnet("PCV_ROUTER")); VoltSystemOracle oracle = VoltSystemOracle( @@ -437,11 +388,6 @@ contract vip16 is Proposal { addresses.mainnet("GLOBAL_RATE_LIMITED_MINTER") ) ); - core.setGlobalSystemExitRateLimiter( - IGlobalSystemExitRateLimiter( - addresses.mainnet("GLOBAL_SYSTEM_EXIT_RATE_LIMITER") - ) - ); core.setPCVOracle(IPCVOracle(addresses.mainnet("PCV_ORACLE"))); core.setGlobalReentrancyLock( IGlobalReentrancyLock(addresses.mainnet("GLOBAL_LOCK")) @@ -451,7 +397,6 @@ contract vip16 is Proposal { core.grantGovernor(addresses.mainnet("TIMELOCK_CONTROLLER")); core.grantGovernor(addresses.mainnet("GOVERNOR")); /// team multisig - core.grantPCVController(addresses.mainnet("PSM_ALLOCATOR")); core.grantPCVController(addresses.mainnet("PCV_GUARDIAN")); core.grantPCVController(addresses.mainnet("PCV_ROUTER")); core.grantPCVController(addresses.mainnet("GOVERNOR")); /// team multisig @@ -462,31 +407,18 @@ contract vip16 is Proposal { core.grantRole(VoltRoles.PCV_MOVER, addresses.mainnet("GOVERNOR")); /// team multisig core.createRole( - VoltRoles.RATE_LIMIT_SYSTEM_EXIT_DEPLETE, + VoltRoles.PSM_MINTER, VoltRoles.GOVERNOR ); core.grantRole( - VoltRoles.RATE_LIMIT_SYSTEM_EXIT_DEPLETE, - addresses.mainnet("PSM_ALLOCATOR") - ); - core.grantRole( - VoltRoles.RATE_LIMIT_SYSTEM_EXIT_DEPLETE, + VoltRoles.PSM_MINTER, addresses.mainnet("PSM_NONCUSTODIAL_DAI") ); core.grantRole( - VoltRoles.RATE_LIMIT_SYSTEM_EXIT_DEPLETE, + VoltRoles.PSM_MINTER, addresses.mainnet("PSM_NONCUSTODIAL_USDC") ); - core.createRole( - VoltRoles.RATE_LIMIT_SYSTEM_EXIT_REPLENISH, - VoltRoles.GOVERNOR - ); - core.grantRole( - VoltRoles.RATE_LIMIT_SYSTEM_EXIT_REPLENISH, - addresses.mainnet("PSM_ALLOCATOR") - ); - core.createRole(VoltRoles.PCV_DEPOSIT, VoltRoles.GOVERNOR); core.grantRole( VoltRoles.PCV_DEPOSIT, @@ -521,38 +453,14 @@ contract vip16 is Proposal { core.grantGuardian(addresses.mainnet("GOVERNOR")); /// team multisig core.grantGuardian(addresses.mainnet("COMPOUND_BAD_DEBT_SENTINEL")); - core.grantRateLimitedMinter(addresses.mainnet("PSM_DAI")); - core.grantRateLimitedMinter(addresses.mainnet("PSM_USDC")); - - core.grantRateLimitedRedeemer(addresses.mainnet("PSM_DAI")); - core.grantRateLimitedRedeemer(addresses.mainnet("PSM_USDC")); - core.grantRateLimitedRedeemer( - addresses.mainnet("PSM_NONCUSTODIAL_DAI") - ); - core.grantRateLimitedRedeemer( - addresses.mainnet("PSM_NONCUSTODIAL_USDC") - ); - - core.grantSystemExitRateLimitDepleter( - addresses.mainnet("PSM_NONCUSTODIAL_DAI") - ); - core.grantSystemExitRateLimitDepleter( - addresses.mainnet("PSM_NONCUSTODIAL_USDC") - ); - - core.grantSystemExitRateLimitReplenisher( - addresses.mainnet("PSM_ALLOCATOR") - ); + core.grantPsmMinter(addresses.mainnet("PSM_NONCUSTODIAL_DAI")); + core.grantPsmMinter(addresses.mainnet("PSM_NONCUSTODIAL_USDC")); core.grantLocker(addresses.mainnet("SYSTEM_ENTRY")); - core.grantLocker(addresses.mainnet("PSM_ALLOCATOR")); core.grantLocker(addresses.mainnet("PCV_ORACLE")); - core.grantLocker(addresses.mainnet("PSM_DAI")); - core.grantLocker(addresses.mainnet("PSM_USDC")); core.grantLocker(addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_DAI")); core.grantLocker(addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_USDC")); core.grantLocker(addresses.mainnet("GLOBAL_RATE_LIMITED_MINTER")); - core.grantLocker(addresses.mainnet("GLOBAL_SYSTEM_EXIT_RATE_LIMITER")); core.grantLocker(addresses.mainnet("PCV_ROUTER")); core.grantLocker(addresses.mainnet("PCV_GUARDIAN")); core.grantLocker(addresses.mainnet("PSM_NONCUSTODIAL_DAI")); @@ -564,26 +472,6 @@ contract vip16 is Proposal { core.grantMinter(addresses.mainnet("GLOBAL_RATE_LIMITED_MINTER")); - /// Allocator config - allocator.connectPSM( - addresses.mainnet("PSM_USDC"), - USDC_TARGET_BALANCE, - USDC_DECIMALS_NORMALIZER - ); - allocator.connectPSM( - addresses.mainnet("PSM_DAI"), - DAI_TARGET_BALANCE, - DAI_DECIMALS_NORMALIZER - ); - allocator.connectDeposit( - addresses.mainnet("PSM_USDC"), - addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_USDC") - ); - allocator.connectDeposit( - addresses.mainnet("PSM_DAI"), - addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_DAI") - ); - /// Configure PCV Oracle address[] memory venues = new address[](6); venues[0] = addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_DAI"); @@ -640,9 +528,6 @@ contract vip16 is Proposal { function validate(Addresses addresses, address /* deployer*/) public { CoreV2 core = CoreV2(addresses.mainnet("CORE")); - ERC20Allocator allocator = ERC20Allocator( - addresses.mainnet("PSM_ALLOCATOR") - ); PCVOracle pcvOracle = PCVOracle(addresses.mainnet("PCV_ORACLE")); PCVRouter pcvRouter = PCVRouter(addresses.mainnet("PCV_ROUTER")); MigratorRouter migratorRouter = MigratorRouter( @@ -684,13 +569,6 @@ contract vip16 is Proposal { ), address(core) ); - assertEq( - address( - CoreRefV2(addresses.mainnet("GLOBAL_SYSTEM_EXIT_RATE_LIMITER")) - .core() - ), - address(core) - ); assertEq( address(CoreRefV2(addresses.mainnet("VOLT_SYSTEM_ORACLE")).core()), address(core) @@ -735,14 +613,6 @@ contract vip16 is Proposal { ), address(core) ); - assertEq( - address(CoreRefV2(addresses.mainnet("PSM_DAI")).core()), - address(core) - ); - assertEq( - address(CoreRefV2(addresses.mainnet("PSM_USDC")).core()), - address(core) - ); assertEq( address( CoreRefV2(addresses.mainnet("PSM_NONCUSTODIAL_USDC")).core() @@ -755,10 +625,6 @@ contract vip16 is Proposal { ), address(core) ); - assertEq( - address(CoreRefV2(addresses.mainnet("PSM_ALLOCATOR")).core()), - address(core) - ); assertEq( address( CoreRefV2(addresses.mainnet("V1_MIGRATION_MIGRATOR")).core() @@ -813,33 +679,8 @@ contract vip16 is Proposal { address(core.globalRateLimitedMinter()), addresses.mainnet("GLOBAL_RATE_LIMITED_MINTER") ); - assertEq( - address(core.globalSystemExitRateLimiter()), - addresses.mainnet("GLOBAL_SYSTEM_EXIT_RATE_LIMITER") - ); assertEq(address(core.pcvOracle()), addresses.mainnet("PCV_ORACLE")); - // psm allocator - assertEq( - allocator.pcvDepositToPSM( - addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_USDC") - ), - addresses.mainnet("PSM_USDC") - ); - assertEq( - allocator.pcvDepositToPSM( - addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_DAI") - ), - addresses.mainnet("PSM_DAI") - ); - (address psmToken1, , ) = allocator.allPSMs( - addresses.mainnet("PSM_DAI") - ); - (address psmToken2, , ) = allocator.allPSMs( - addresses.mainnet("PSM_USDC") - ); - assertEq(psmToken1, addresses.mainnet("DAI")); - assertEq(psmToken2, addresses.mainnet("USDC")); // pcv oracle assertEq(pcvOracle.getVenues().length, 6); @@ -875,11 +716,11 @@ contract vip16 is Proposal { // oracle references assertEq( - address(PegStabilityModule(addresses.mainnet("PSM_DAI")).oracle()), + address(IOracleRefV2(addresses.mainnet("PSM_NONCUSTODIAL_DAI")).oracle()), addresses.mainnet("VOLT_SYSTEM_ORACLE") ); assertEq( - address(PegStabilityModule(addresses.mainnet("PSM_USDC")).oracle()), + address(IOracleRefV2(addresses.mainnet("PSM_NONCUSTODIAL_USDC")).oracle()), addresses.mainnet("VOLT_SYSTEM_ORACLE") ); diff --git a/test/unit/core/CoreV2.t.sol b/test/unit/core/CoreV2.t.sol index f694432c7..176144020 100644 --- a/test/unit/core/CoreV2.t.sol +++ b/test/unit/core/CoreV2.t.sol @@ -15,7 +15,6 @@ import {getCoreV2} from "@test/unit/utils/Fixtures.sol"; import {IPCVOracle} from "@voltprotocol/oracle/IPCVOracle.sol"; import {IGlobalRateLimitedMinter} from "@voltprotocol/rate-limits/IGlobalRateLimitedMinter.sol"; import {TestAddresses as addresses} from "@test/unit/utils/TestAddresses.sol"; -import {IGlobalSystemExitRateLimiter} from "@voltprotocol/rate-limits/IGlobalSystemExitRateLimiter.sol"; import {IGlobalReentrancyLock, GlobalReentrancyLock} from "@voltprotocol/core/GlobalReentrancyLock.sol"; contract UnitTestCoreV2 is Test { @@ -42,12 +41,6 @@ contract UnitTestCoreV2 is Test { address indexed newPcvOracle ); - /// @notice emitted when reference to global system exit rate limiter is updated - event GlobalSystemExitRateLimiterUpdate( - address indexed oldGserl, - address indexed newGserl - ); - function setUp() public { core = getCoreV2(); vcon = address(core.vcon()); @@ -109,26 +102,6 @@ contract UnitTestCoreV2 is Test { ); } - function testGovernorSetsGlobalSystemExitRateLimiter() public { - address newGserl = address(103927828732); - vm.expectEmit(true, true, false, true, address(core)); - emit GlobalSystemExitRateLimiterUpdate(address(0), newGserl); - - vm.prank(addresses.governorAddress); - core.setGlobalSystemExitRateLimiter( - IGlobalSystemExitRateLimiter(newGserl) - ); - - assertEq(address(core.globalSystemExitRateLimiter()), newGserl); - } - - function testNonGovernorFailsSettingGlobalSystemExitRateLimiter() public { - vm.expectRevert("Permissions: Caller is not a governor"); - core.setGlobalSystemExitRateLimiter( - IGlobalSystemExitRateLimiter(addresses.userAddress) - ); - } - function testGovernorSetsPcvOracle() public { address newPcvOracle = address(8794534168787); vm.expectEmit(true, true, false, true, address(core)); diff --git a/test/unit/core/PermissionsV2.t.sol b/test/unit/core/PermissionsV2.t.sol index cfbe8fa25..985c9e26c 100644 --- a/test/unit/core/PermissionsV2.t.sol +++ b/test/unit/core/PermissionsV2.t.sol @@ -309,93 +309,24 @@ contract UnitTestPermissionsV2 is Test { function testGovAddsMinterRoleSucceeds() public { vm.prank(addresses.governorAddress); - core.grantRateLimitedMinter(address(this)); - assertTrue(core.isRateLimitedMinter(address(this))); + core.grantPsmMinter(address(this)); + assertTrue(core.isPsmMinter(address(this))); } function testGovRevokesMinterRoleSucceeds() public { testGovAddsMinterRoleSucceeds(); vm.prank(addresses.governorAddress); - core.revokeRateLimitedMinter(address(this)); - assertTrue(!core.isRateLimitedMinter(address(this))); + core.revokePsmMinter(address(this)); + assertTrue(!core.isPsmMinter(address(this))); } function testNonGovAddsMinterRoleFails() public { vm.expectRevert("Permissions: Caller is not a governor"); - core.grantRateLimitedMinter(address(this)); + core.grantPsmMinter(address(this)); } function testNonGovRevokesMinterRoleFails() public { vm.expectRevert("Permissions: Caller is not a governor"); - core.revokeRateLimitedMinter(address(this)); - } - - function testGovAddsRedeemerRoleSucceeds() public { - vm.prank(addresses.governorAddress); - core.grantRateLimitedRedeemer(address(this)); - assertTrue(core.isRateLimitedRedeemer(address(this))); - } - - function testGovRevokesRedeemerRoleSucceeds() public { - testGovAddsRedeemerRoleSucceeds(); - vm.prank(addresses.governorAddress); - core.revokeRateLimitedRedeemer(address(this)); - assertTrue(!core.isRateLimitedRedeemer(address(this))); - } - - function testNonGovAddsRedeemerRoleFails() public { - vm.expectRevert("Permissions: Caller is not a governor"); - core.grantRateLimitedRedeemer(address(this)); - } - - function testNonGovRevokesRedeemerRoleFails() public { - vm.expectRevert("Permissions: Caller is not a governor"); - core.revokeRateLimitedRedeemer(address(this)); - } - - function testGovAddsRateLimitedDepleterRoleSucceeds() public { - vm.prank(addresses.governorAddress); - core.grantSystemExitRateLimitDepleter(address(this)); - assertTrue(core.isSystemExitRateLimitDepleter(address(this))); - } - - function testGovRevokesRateLimitedDepleterRoleSucceeds() public { - testGovAddsRedeemerRoleSucceeds(); - vm.prank(addresses.governorAddress); - core.revokeSystemExitRateLimitDepleter(address(this)); - assertTrue(!core.isSystemExitRateLimitDepleter(address(this))); - } - - function testNonGovAddsRateLimitedDepleterRoleFails() public { - vm.expectRevert("Permissions: Caller is not a governor"); - core.grantSystemExitRateLimitDepleter(address(this)); - } - - function testNonGovRevokesRateLimitedDepleterRoleFails() public { - vm.expectRevert("Permissions: Caller is not a governor"); - core.revokeSystemExitRateLimitDepleter(address(this)); - } - - function testGovAddsRateLimitedReplenisherRoleSucceeds() public { - vm.prank(addresses.governorAddress); - core.grantSystemExitRateLimitReplenisher(address(this)); - assertTrue(core.isSystemExitRateLimitReplenisher(address(this))); - } - - function testGovRevokesRateLimitedReplenisherRoleSucceeds() public { - testGovAddsRedeemerRoleSucceeds(); - vm.prank(addresses.governorAddress); - core.revokeSystemExitRateLimitReplenisher(address(this)); - assertTrue(!core.isSystemExitRateLimitReplenisher(address(this))); - } - - function testNonGovAddsRateLimitedReplenisherRoleFails() public { - vm.expectRevert("Permissions: Caller is not a governor"); - core.grantSystemExitRateLimitReplenisher(address(this)); - } - - function testNonGovRevokesRateLimitedReplenisherRoleFails() public { - vm.expectRevert("Permissions: Caller is not a governor"); - core.revokeSystemExitRateLimitReplenisher(address(this)); + core.revokePsmMinter(address(this)); } } diff --git a/test/unit/limiter/GlobalRateLimitedMinter.t.sol b/test/unit/limiter/GlobalRateLimitedMinter.t.sol index 3f131c58e..1b48cd5e7 100644 --- a/test/unit/limiter/GlobalRateLimitedMinter.t.sol +++ b/test/unit/limiter/GlobalRateLimitedMinter.t.sol @@ -39,10 +39,10 @@ contract GlobalRateLimitedMinterUnitTest is Test { uint256 public constant maxRateLimitPerSecondMinting = 100e18; /// replenish 500k VOLT per day - uint128 public constant rateLimitPerSecondMinting = 5.787e18; + uint64 public constant rateLimitPerSecondMinting = 5.787e18; /// buffer cap of 1.5m VOLT - uint128 public constant bufferCapMinting = 1_500_000e18; + uint96 public constant bufferCapMinting = 1_500_000e18; function setUp() public { vm.warp(1); /// warp past 0 @@ -65,12 +65,10 @@ contract GlobalRateLimitedMinterUnitTest is Test { core.grantMinter(address(grlm)); core.grantLocker(address(grlm)); - core.grantRateLimitedMinter(guardianAddresses.pcvGuardAddress1); - core.grantRateLimitedMinter(guardianAddresses.pcvGuardAddress2); - core.grantRateLimitedRedeemer(guardianAddresses.pcvGuardAddress1); - core.grantRateLimitedRedeemer(guardianAddresses.pcvGuardAddress2); + core.grantPsmMinter(guardianAddresses.pcvGuardAddress1); + core.grantPsmMinter(guardianAddresses.pcvGuardAddress2); + core.grantPsmMinter(address(minter)); - core.grantRateLimitedMinter(address(minter)); core.grantLocker(address(minter)); core.setGlobalRateLimitedMinter( @@ -88,10 +86,10 @@ contract GlobalRateLimitedMinterUnitTest is Test { function testSetup() public { assertTrue(core.isMinter(address(grlm))); assertTrue( - core.isRateLimitedMinter(guardianAddresses.pcvGuardAddress1) + core.isPsmMinter(guardianAddresses.pcvGuardAddress1) ); assertTrue( - core.isRateLimitedMinter(guardianAddresses.pcvGuardAddress2) + core.isPsmMinter(guardianAddresses.pcvGuardAddress2) ); assertEq(address(core.globalRateLimitedMinter()), address(grlm)); @@ -134,7 +132,7 @@ contract GlobalRateLimitedMinterUnitTest is Test { uint80 depleteAmount ) public { vm.prank(addresses.governorAddress); - core.grantRateLimitedRedeemer(address(minter)); + core.grantPsmMinter(address(minter)); minter.mint(address(this), depleteAmount); diff --git a/test/unit/limiter/GlobalSystemExitRateLimiter.t.sol b/test/unit/limiter/GlobalSystemExitRateLimiter.t.sol deleted file mode 100644 index 408b58ac9..000000000 --- a/test/unit/limiter/GlobalSystemExitRateLimiter.t.sol +++ /dev/null @@ -1,158 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.13; - -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; - -import {Test} from "@forge-std/Test.sol"; -import {CoreV2} from "@voltprotocol/core/CoreV2.sol"; -import {TestAddresses} from "@test/unit/utils/TestAddresses.sol"; -import {TestAddresses as addresses} from "@test/unit/utils/TestAddresses.sol"; -import {IGlobalSystemExitRateLimiter, GlobalSystemExitRateLimiter} from "@voltprotocol/rate-limits/GlobalSystemExitRateLimiter.sol"; -import {getCoreV2, getVoltAddresses, VoltAddresses} from "@test/unit/utils/Fixtures.sol"; -import {IGlobalReentrancyLock, GlobalReentrancyLock} from "@voltprotocol/core/GlobalReentrancyLock.sol"; - -/// deployment steps -/// 1. core v2 -/// 2. Global Rate Limited Minter - -/// setup steps -/// 1. grant minter role to Global Rate Limited Minter -/// 2. grant global rate limited minter role to 2 EOA's - -contract GlobalSystemExitRateLimiterUnitTest is Test { - using SafeCast for *; - - VoltAddresses public guardianAddresses = getVoltAddresses(); - - CoreV2 private core; - IERC20 private volt; - address private coreAddress; - GlobalReentrancyLock public lock; - GlobalSystemExitRateLimiter public gserl; - - /// ---------- GSERL PARAMS ---------- - - /// maximum rate limit per second is 100 USD - uint256 public constant maxRateLimitPerSecond = 100e18; - - /// replenish 500k USD per day - uint128 public constant rateLimitPerSecond = 5.787e18; - - /// buffer cap of 1.5m USD - uint128 public constant bufferCap = 1_500_000e18; - - function setUp() public { - vm.warp(1); /// warp past 0 - core = getCoreV2(); - coreAddress = address(core); - lock = new GlobalReentrancyLock(coreAddress); - volt = core.volt(); - gserl = new GlobalSystemExitRateLimiter( - coreAddress, - maxRateLimitPerSecond, - rateLimitPerSecond, - bufferCap - ); - - vm.startPrank(addresses.governorAddress); - - core.setGlobalReentrancyLock(IGlobalReentrancyLock(address(lock))); - - core.grantLocker(address(this)); - core.grantLocker(address(gserl)); - - core.grantRateLimitedMinter(guardianAddresses.pcvGuardAddress1); - core.grantRateLimitedMinter(guardianAddresses.pcvGuardAddress2); - core.grantRateLimitedRedeemer(guardianAddresses.pcvGuardAddress1); - core.grantRateLimitedRedeemer(guardianAddresses.pcvGuardAddress2); - - core.setGlobalSystemExitRateLimiter( - IGlobalSystemExitRateLimiter(address(gserl)) - ); - - vm.stopPrank(); - - vm.label(address(gserl), "gserl"); - vm.label(address(core), "core"); - vm.label(address(this), "address this"); - } - - function testSetup() public { - assertEq(gserl.buffer(), bufferCap); - assertEq(gserl.rateLimitPerSecond(), rateLimitPerSecond); - assertEq(gserl.MAX_RATE_LIMIT_PER_SECOND(), maxRateLimitPerSecond); - - assertTrue(core.isLocker(address(gserl))); - assertTrue( - core.isRateLimitedMinter(guardianAddresses.pcvGuardAddress1) - ); - assertTrue( - core.isRateLimitedMinter(guardianAddresses.pcvGuardAddress2) - ); - - assertEq(address(core.globalSystemExitRateLimiter()), address(gserl)); - } - - function testDepleteBufferNonDepleterFails() public { - vm.expectRevert("UNAUTHORIZED"); - gserl.depleteBuffer(100); - } - - function testReplenishBufferNonDepleterFails() public { - vm.expectRevert("UNAUTHORIZED"); - gserl.replenishBuffer(100); - } - - function testReplenishAsReplenisherFailsWhenNotLocked() public { - vm.prank(TestAddresses.governorAddress); - core.grantSystemExitRateLimitReplenisher(address(this)); - - vm.expectRevert("GlobalReentrancyLock: invalid lock level"); - gserl.replenishBuffer(0); - } - - function testDepleteAsDepleterFailsWhenNotLocked() public { - vm.prank(TestAddresses.governorAddress); - core.grantSystemExitRateLimitDepleter(address(this)); - - vm.expectRevert("GlobalReentrancyLock: invalid lock level"); - gserl.depleteBuffer(0); - } - - function testDepleteAsDepleterSucceeds(uint80 amount) public { - vm.prank(TestAddresses.governorAddress); - core.grantSystemExitRateLimitDepleter(address(this)); - - uint256 startingBuffer = gserl.buffer(); - - lock.lock(1); - gserl.depleteBuffer(amount); - - uint256 endingBuffer = gserl.buffer(); - - assertEq(endingBuffer, startingBuffer - amount); - } - - function testReplenishAsReplenisherSucceeds( - uint80 replenishAmount, - uint80 depleteAmount - ) public { - testDepleteAsDepleterSucceeds(depleteAmount); - - vm.prank(addresses.governorAddress); - core.grantSystemExitRateLimitReplenisher(address(this)); - - uint256 startingBuffer = gserl.buffer(); - - gserl.replenishBuffer(replenishAmount); - - uint256 endingBuffer = gserl.buffer(); - - assertEq( - endingBuffer, - Math.min(startingBuffer + replenishAmount, gserl.bufferCap()) - ); - } -} diff --git a/test/unit/pcv/utils/ERC20Allocator.t.sol b/test/unit/pcv/utils/ERC20Allocator.t.sol deleted file mode 100644 index 480394ac7..000000000 --- a/test/unit/pcv/utils/ERC20Allocator.t.sol +++ /dev/null @@ -1,987 +0,0 @@ -pragma solidity =0.8.13; - -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -import {Vm} from "@forge-std/Vm.sol"; -import {Test} from "@forge-std/Test.sol"; -import {CoreV2} from "@voltprotocol/core/CoreV2.sol"; -import {getCoreV2} from "@test/unit/utils/Fixtures.sol"; -import {MockERC20} from "@test/mock/MockERC20.sol"; -import {VoltRoles} from "@voltprotocol/core/VoltRoles.sol"; -import {PCVDeposit} from "@voltprotocol/pcv/PCVDeposit.sol"; -import {ERC20Allocator} from "@voltprotocol/pcv/ERC20Allocator.sol"; -import {ERC20HoldingPCVDeposit} from "@test/mock/ERC20HoldingPCVDeposit.sol"; -import {TestAddresses as addresses} from "@test/unit/utils/TestAddresses.sol"; -import {IGlobalReentrancyLock, GlobalReentrancyLock} from "@voltprotocol/core/GlobalReentrancyLock.sol"; -import {IGlobalSystemExitRateLimiter, GlobalSystemExitRateLimiter} from "@voltprotocol/rate-limits/GlobalSystemExitRateLimiter.sol"; - -contract UnitTestERC20Allocator is Test { - /// @notice emitted when an existing deposit is updated - event PSMTargetBalanceUpdated(address psm, uint248 targetBalance); - - /// @notice PSM deletion event - event PSMDeleted(address psm); - - /// @notice event emitted when tokens are dripped - event Dripped(uint256 amount); - - /// @notice event emitted in do action when neither skim nor drip could be triggered - event NoOp(); - - /// @notice emitted when an existing deposit is deleted - event DepositDeleted(address psm); - - CoreV2 private core; - - /// @notice reference to the PCVDeposit to pull from - ERC20HoldingPCVDeposit private pcvDeposit; - - /// @notice reference to the PCVDeposit to push to - ERC20HoldingPCVDeposit private psm; - - /// @notice reference to the ERC20 - ERC20Allocator private allocator; - - /// @notice reference to global system exit rate limiter - GlobalSystemExitRateLimiter private gserl; - - /// @notice token to push - MockERC20 private token; - - /// @notice global reentrancy lock - IGlobalReentrancyLock private lock; - - /// @notice threshold over which to pull tokens from pull deposit - uint248 private constant targetBalance = 100_000e18; - - /// @notice maximum rate limit per second in RateLimitedV2 - uint256 private constant maxRateLimitPerSecond = 1_000_000e18; - - /// @notice rate limit per second in RateLimitedV2 - uint128 private constant rateLimitPerSecond = 10_000e18; - - /// @notice buffer cap in RateLimitedV2 - uint128 private constant bufferCap = 10_000_000e18; - - function setUp() public { - core = getCoreV2(); - token = new MockERC20(); - - lock = IGlobalReentrancyLock( - address(new GlobalReentrancyLock(address(core))) - ); - - pcvDeposit = new ERC20HoldingPCVDeposit( - address(core), - IERC20(address(token)), - address(0) - ); - - psm = new ERC20HoldingPCVDeposit( - address(core), - IERC20(address(token)), - address(0) - ); - - gserl = new GlobalSystemExitRateLimiter( - address(core), - maxRateLimitPerSecond, - rateLimitPerSecond, - bufferCap - ); - - allocator = new ERC20Allocator(address(core)); - - vm.startPrank(addresses.governorAddress); - - allocator.connectPSM(address(psm), targetBalance, 0); - allocator.connectDeposit(address(psm), address(pcvDeposit)); - core.grantLocker(address(allocator)); - core.grantLocker(address(gserl)); - core.grantSystemExitRateLimitDepleter(address(allocator)); - core.grantSystemExitRateLimitReplenisher(address(allocator)); - - core.setGlobalSystemExitRateLimiter( - IGlobalSystemExitRateLimiter(address(gserl)) - ); - core.setGlobalReentrancyLock(lock); - - vm.stopPrank(); - } - - function testSetup() public { - assertEq(address(allocator.core()), address(core)); - ( - address psmToken, - uint248 psmTargetBalance, - int8 decimalsNormalizer - ) = allocator.allPSMs(address(psm)); - - address psmAddress = allocator.pcvDepositToPSM(address(pcvDeposit)); - - assertEq(psmTargetBalance, targetBalance); - assertEq(decimalsNormalizer, 0); - assertEq(psmToken, address(token)); - assertEq(gserl.buffer(), bufferCap); - assertEq(targetBalance, allocator.targetBalance(address(psm))); - assertEq(psmAddress, address(psm)); - - assertTrue(!allocator.checkDripCondition(address(pcvDeposit))); /// drip action not allowed, due to 0 balance - assertTrue(!allocator.checkSkimCondition(address(pcvDeposit))); /// skim action not allowed, not over threshold - assertTrue(!allocator.checkActionAllowed(address(pcvDeposit))); /// neither drip nor skim action allowed - } - - function testSkimFailsWhenUnderFunded() public { - assertTrue(!allocator.checkSkimCondition(address(pcvDeposit))); - assertTrue(!allocator.checkDripCondition(address(pcvDeposit))); /// drip action not allowed, due to 0 balance - assertTrue(!allocator.checkActionAllowed(address(pcvDeposit))); - - vm.expectRevert("ERC20Allocator: skim condition not met"); - allocator.skim(address(pcvDeposit)); - } - - function testDripFailsWhenUnderFunded() public { - vm.prank(addresses.governorAddress); - core.grantPCVController(address(allocator)); - token.mint(address(psm), targetBalance * 2); - - assertTrue(!allocator.checkDripCondition(address(pcvDeposit))); - assertTrue(allocator.checkSkimCondition(address(pcvDeposit))); - assertTrue(allocator.checkActionAllowed(address(pcvDeposit))); - - vm.expectRevert("ERC20Allocator: drip condition not met"); - allocator.drip(address(pcvDeposit)); - } - - function testDripFailsWhenBufferExhausted() public { - vm.startPrank(addresses.governorAddress); - core.grantPCVController(address(allocator)); - gserl.setBufferCap(uint128(targetBalance)); /// only allow 1 complete drip to exhaust buffer - - token.mint(address(pcvDeposit), targetBalance * 2); - - assertTrue(allocator.checkDripCondition(address(pcvDeposit))); - assertTrue(!allocator.checkSkimCondition(address(pcvDeposit))); /// cannot skim - assertTrue(allocator.checkActionAllowed(address(pcvDeposit))); - - allocator.drip(address(pcvDeposit)); - - assertTrue(!allocator.checkDripCondition(address(pcvDeposit))); - assertTrue(!allocator.checkSkimCondition(address(pcvDeposit))); /// cannot skim - assertTrue(!allocator.checkActionAllowed(address(pcvDeposit))); - assertEq(gserl.buffer(), 0); - - token.mint(address(psm), targetBalance); - - assertEq(psm.balance(), targetBalance * 2); - assertTrue(allocator.checkSkimCondition(address(pcvDeposit))); - assertTrue(allocator.checkActionAllowed(address(pcvDeposit))); - assertTrue(!allocator.checkDripCondition(address(pcvDeposit))); /// cannot drip as buffer is exhausted - - token.mockBurn(address(psm), token.balanceOf(address(psm))); - assertEq(gserl.buffer(), 0); - - vm.expectRevert("RateLimited: no rate limit buffer"); - allocator.drip(address(pcvDeposit)); - } - - function testDripFailsWhenBufferZero() public { - vm.startPrank(addresses.governorAddress); - core.grantPCVController(address(allocator)); - gserl.setBufferCap(uint128(0)); /// fully exhaust buffer - - token.mint(address(pcvDeposit), targetBalance * 2); - - assertTrue(!allocator.checkDripCondition(address(pcvDeposit))); - assertTrue(!allocator.checkSkimCondition(address(pcvDeposit))); /// cannot skim - assertTrue(!allocator.checkActionAllowed(address(pcvDeposit))); - assertEq(gserl.buffer(), 0); - - vm.expectRevert("RateLimited: no rate limit buffer"); - allocator.drip(address(pcvDeposit)); - } - - function testDripSucceedsWhenBufferFiftyPercentDepleted() public { - vm.startPrank(addresses.governorAddress); - core.grantPCVController(address(allocator)); - gserl.setBufferCap(uint128(targetBalance / 2)); /// halfway exhaust buffer - - token.mint(address(pcvDeposit), targetBalance * 2); - - assertTrue(allocator.checkDripCondition(address(pcvDeposit))); - assertTrue(!allocator.checkSkimCondition(address(pcvDeposit))); /// cannot skim - assertTrue(allocator.checkActionAllowed(address(pcvDeposit))); - assertEq(gserl.buffer(), targetBalance / 2); - - allocator.drip(address(pcvDeposit)); - - assertEq(psm.balance(), targetBalance / 2); - } - - function testDripSucceedsWhenBufferFiftyPercentDepletedDecimalsNormalized() - public - { - int8 decimalsNormalizer = 12; /// scale up new token by 12 decimals - uint248 newTargetBalance = 100_000e6; /// target balance 100k - - MockERC20 newToken = new MockERC20(); - ERC20HoldingPCVDeposit newPsm = new ERC20HoldingPCVDeposit( - address(core), - IERC20(address(newToken)), - address(0) - ); - ERC20HoldingPCVDeposit newPcvDeposit = new ERC20HoldingPCVDeposit( - address(core), - IERC20(address(newToken)), - address(0) - ); - - vm.startPrank(addresses.governorAddress); - allocator.connectPSM( - address(newPsm), - newTargetBalance, - decimalsNormalizer - ); - allocator.connectDeposit(address(newPsm), address(newPcvDeposit)); - vm.stopPrank(); - - vm.startPrank(addresses.governorAddress); - core.grantPCVController(address(allocator)); - gserl.setBufferCap(uint128(targetBalance / 2)); /// halfway exhaust buffer - - newToken.mint(address(newPcvDeposit), newTargetBalance * 2); - - assertTrue(allocator.checkDripCondition(address(newPcvDeposit))); - assertTrue(!allocator.checkSkimCondition(address(newPcvDeposit))); /// cannot skim - assertTrue(allocator.checkActionAllowed(address(newPcvDeposit))); - assertEq(gserl.buffer(), targetBalance / 2); - - (uint256 amountToDrip, uint256 adjustedAmountToDrip) = allocator - .getDripDetails(address(newPsm), address(newPcvDeposit)); - - allocator.drip(address(newPcvDeposit)); - - assertEq(adjustedAmountToDrip, amountToDrip * 1e12); - assertEq(newPsm.balance(), amountToDrip); - assertEq(newPsm.balance(), newTargetBalance / 2); - assertEq(gserl.buffer(), 0); - } - - function testDripSucceedsWhenBufferFiftyPercentDepletedDecimalsNormalizedNegative() - public - { - int8 decimalsNormalizer = -12; /// scale down new token by 12 decimals - uint248 newTargetBalance = 100_000e30; /// target balance 100k - - MockERC20 newToken = new MockERC20(); - ERC20HoldingPCVDeposit newPsm = new ERC20HoldingPCVDeposit( - address(core), - IERC20(address(newToken)), - address(0) - ); - ERC20HoldingPCVDeposit newPcvDeposit = new ERC20HoldingPCVDeposit( - address(core), - IERC20(address(newToken)), - address(0) - ); - - vm.startPrank(addresses.governorAddress); - allocator.connectPSM( - address(newPsm), - newTargetBalance, - decimalsNormalizer - ); - allocator.connectDeposit(address(newPsm), address(newPcvDeposit)); - vm.stopPrank(); - - vm.startPrank(addresses.governorAddress); - core.grantPCVController(address(allocator)); - gserl.setBufferCap(uint128(targetBalance / 2)); /// halfway exhaust buffer - - newToken.mint(address(newPcvDeposit), newTargetBalance * 2); - - assertTrue(allocator.checkDripCondition(address(newPcvDeposit))); - assertTrue(!allocator.checkSkimCondition(address(newPcvDeposit))); /// cannot skim - assertTrue(allocator.checkActionAllowed(address(newPcvDeposit))); - assertEq(gserl.buffer(), targetBalance / 2); - - (uint256 amountToDrip, uint256 adjustedAmountToDrip) = allocator - .getDripDetails(address(newPsm), address(newPcvDeposit)); - - allocator.drip(address(newPcvDeposit)); - - assertEq(adjustedAmountToDrip, amountToDrip / 1e12); - assertEq(newPsm.balance(), amountToDrip); - assertEq(newPsm.balance(), newTargetBalance / 2); - assertEq(gserl.buffer(), 0); /// buffer has been fully drained - } - - function testCreateDepositNonGovFails() public { - vm.expectRevert("CoreRef: Caller is not a governor"); - allocator.connectPSM(address(0), 0, 0); - } - - function testeditPSMTargetBalanceNonGovFails() public { - vm.expectRevert("CoreRef: Caller is not a governor"); - allocator.editPSMTargetBalance(address(0), 0); - } - - function testConnectDepositNonGovFails() public { - vm.expectRevert("CoreRef: Caller is not a governor"); - allocator.connectDeposit(address(0), address(0)); /// params don't matter as call reverts - } - - function testDeleteDepositNonGovFails() public { - vm.expectRevert("CoreRef: Caller is not a governor"); - allocator.deleteDeposit(address(0)); - } - - function testDeletePSMNonGovFails() public { - vm.expectRevert("CoreRef: Caller is not a governor"); - allocator.disconnectPSM(address(psm)); - } - - function _disconnectPSM() internal { - vm.prank(addresses.governorAddress); - vm.expectEmit(true, false, false, true, address(allocator)); - emit PSMDeleted(address(psm)); - allocator.disconnectPSM(address(psm)); - - ( - address psmToken, - uint248 psmTargetBalance, - int8 decimalsNormalizer - ) = allocator.allPSMs(address(psm)); - /// this part of the mapping doesn't get deleted when PSM is deleted - address psmAddress = allocator.pcvDepositToPSM(address(pcvDeposit)); - - assertEq(psmTargetBalance, 0); - assertEq(decimalsNormalizer, 0); - assertEq(psmToken, address(0)); - assertEq(psmAddress, address(psm)); - } - - function testDeletePSMGovSucceeds() public { - _disconnectPSM(); - } - - /// test that you can no longer skim to this psm when pcv deposits - /// are still connected to a non existent psm - function testDeletePSMGovSucceedsSkimFails() public { - _disconnectPSM(); - - vm.expectRevert(); - allocator.skim(address(pcvDeposit)); - } - - /// test that you can no longer drip to this psm when pcv deposits - /// are still connected to a non existent psm - function testDeletePSMGovSucceedsDripFails() public { - _disconnectPSM(); - - vm.expectRevert(); - allocator.drip(address(pcvDeposit)); - } - - /// test that you can no longer skim to this psm when pcv deposits - /// are not connected to a non existent psm - function testDeletePSMGovSucceedsSkimFailsDeleteDeposit() public { - _disconnectPSM(); - vm.prank(addresses.governorAddress); - allocator.deleteDeposit(address(pcvDeposit)); - - /// connection broken - address psmAddress = allocator.pcvDepositToPSM(address(pcvDeposit)); - assertEq(psmAddress, address(0)); - - vm.expectRevert("ERC20Allocator: invalid PCVDeposit"); - allocator.skim(address(pcvDeposit)); - } - - /// test that you can no longer drip to this psm when pcv deposits - /// are not connected to a non existent psm - function testDeletePSMGovSucceedsDripFailsDeleteDeposit() public { - _disconnectPSM(); - vm.prank(addresses.governorAddress); - allocator.deleteDeposit(address(pcvDeposit)); - - vm.expectRevert("ERC20Allocator: invalid PCVDeposit"); - allocator.drip(address(pcvDeposit)); - } - - function testDeleteDepositGovSucceeds() public { - vm.prank(addresses.governorAddress); - vm.expectEmit(true, false, false, true, address(allocator)); - emit DepositDeleted(address(pcvDeposit)); - allocator.deleteDeposit(address(pcvDeposit)); - - address psmDeposit = allocator.pcvDepositToPSM(address(psm)); - assertEq(psmDeposit, address(0)); - } - - function testDripAndSkimFailsWhenPaused() public { - vm.prank(addresses.governorAddress); - allocator.pause(); - - vm.expectRevert("Pausable: paused"); - allocator.skim(address(pcvDeposit)); - - vm.expectRevert("Pausable: paused"); - allocator.drip(address(pcvDeposit)); - } - - function testSkimFailsWhenOverTargetWithoutPCVController() public { - uint256 depositBalance = 10_000_000e18; - - token.mint(address(psm), depositBalance); - - vm.expectRevert("UNAUTHORIZED"); - allocator.skim(address(pcvDeposit)); - } - - function testDripperFailsWhenUnderFunded() public { - assertTrue(!allocator.checkActionAllowed(address(pcvDeposit))); - assertTrue(!allocator.checkDripCondition(address(pcvDeposit))); - assertTrue(!allocator.checkSkimCondition(address(pcvDeposit))); - - vm.prank(addresses.governorAddress); - core.grantPCVController(address(allocator)); - - vm.expectRevert("ERC20Allocator: drip condition not met"); - allocator.drip(address(pcvDeposit)); - } - - function testDripNoOpWhenUnderTargetWithoutPCVControllerRole() public { - assertTrue(!allocator.checkActionAllowed(address(pcvDeposit))); - assertTrue(!allocator.checkDripCondition(address(pcvDeposit))); - assertTrue(!allocator.checkSkimCondition(address(pcvDeposit))); - - vm.expectRevert("ERC20Allocator: drip condition not met"); - allocator.drip(address(pcvDeposit)); - } - - function testDripFailsWhenUnderTargetWithoutPCVControllerRole() public { - token.mint(address(pcvDeposit), 1); /// with a balance of 1, the drip action is valid - - assertTrue(allocator.checkActionAllowed(address(pcvDeposit))); - assertTrue(allocator.checkDripCondition(address(pcvDeposit))); - assertTrue(!allocator.checkSkimCondition(address(pcvDeposit))); - - vm.expectRevert("UNAUTHORIZED"); - allocator.drip(address(pcvDeposit)); - } - - function testDoActionNoOpWhenUnderTargetWithoutPCVControllerRole() public { - /// if this action was valid, it would fail because it doesn't have the pcv controller role - allocator.doAction(address(pcvDeposit)); - } - - function testDoActionFailsWhenUnderTargetWithoutPCVControllerRole() public { - token.mint(address(pcvDeposit), 1); /// with a balance of 1, the drip action is valid - assertTrue(allocator.checkActionAllowed(address(pcvDeposit))); - assertTrue(allocator.checkDripCondition(address(pcvDeposit))); - vm.expectRevert("UNAUTHORIZED"); - allocator.doAction(address(pcvDeposit)); - } - - function testTargetBalanceGovSucceeds() public { - uint248 newThreshold = 10_000_000e18; - vm.expectEmit(false, false, false, true, address(allocator)); - emit PSMTargetBalanceUpdated(address(psm), newThreshold); - vm.prank(addresses.governorAddress); - allocator.editPSMTargetBalance(address(psm), newThreshold); - assertEq(uint256(newThreshold), allocator.targetBalance(address(psm))); - } - - function testSweepGovSucceeds() public { - uint256 mintAmount = 100_000_000e18; - token.mint(address(allocator), mintAmount); - vm.prank(addresses.governorAddress); - allocator.sweep(address(token), address(this), mintAmount); - assertEq(token.balanceOf(address(this)), mintAmount); - } - - function testSweepNonGovFails() public { - vm.expectRevert("CoreRef: Caller is not a governor"); - allocator.sweep(address(token), address(this), 0); - } - - function testPullSucceedsWhenOverThresholdWithPCVController() public { - uint256 depositBalance = 10_000_000e18; - - vm.prank(addresses.governorAddress); - core.grantPCVController(address(allocator)); - - token.mint(address(psm), depositBalance); - - assertTrue(allocator.checkSkimCondition(address(pcvDeposit))); - allocator.skim(address(pcvDeposit)); - assertTrue(!allocator.checkSkimCondition(address(pcvDeposit))); - - assertEq(token.balanceOf(address(psm)), targetBalance); - assertEq( - token.balanceOf(address(pcvDeposit)), - depositBalance - targetBalance - ); - } - - function testDripSucceedsWhenOverThreshold() public { - uint256 depositBalance = 10_000_000e18; - - token.mint(address(pcvDeposit), depositBalance); - - vm.prank(addresses.governorAddress); - core.grantPCVController(address(allocator)); - allocator.drip(address(pcvDeposit)); - - assertEq( - token.balanceOf(address(pcvDeposit)), - depositBalance - targetBalance - ); - assertEq(token.balanceOf(address(psm)), targetBalance); - } - - function testDripSucceedsWhenOverThresholdAndPSMPartiallyFunded() public { - uint256 depositBalance = 10_000_000e18; - uint256 bufferStart = gserl.buffer(); - - token.mint(address(pcvDeposit), depositBalance); - token.mint(address(psm), targetBalance / 2); - - vm.prank(addresses.governorAddress); - core.grantPCVController(address(allocator)); - - vm.prank(addresses.governorAddress); - allocator.pause(); - - /// actions not allowed while paused - assertTrue(!allocator.checkActionAllowed(address(pcvDeposit))); - - vm.prank(addresses.governorAddress); - allocator.unpause(); - - assertTrue(allocator.checkActionAllowed(address(pcvDeposit))); - - allocator.drip(address(pcvDeposit)); - assertTrue(!allocator.checkDripCondition(address(pcvDeposit))); - assertTrue(!allocator.checkActionAllowed(address(pcvDeposit))); - - uint256 bufferEnd = gserl.buffer(); - - assertEq(bufferEnd, bufferStart - targetBalance / 2); - assertEq(bufferStart, uint256(bufferCap)); - assertEq( - token.balanceOf(address(pcvDeposit)), - depositBalance - targetBalance / 2 - ); - assertEq(token.balanceOf(address(psm)), targetBalance); - } - - function testAllConditionsFalseWhenPaused() public { - uint256 depositBalance = 10_000_000e18; - - token.mint(address(pcvDeposit), depositBalance); - token.mint(address(psm), targetBalance / 2); - - /// drip condition becomes false when contract is paused - assertTrue(allocator.checkDripCondition(address(pcvDeposit))); - - vm.prank(addresses.governorAddress); - allocator.pause(); - - /// actions not allowed while paused - assertTrue(!allocator.checkDripCondition(address(pcvDeposit))); - assertTrue(!allocator.checkActionAllowed(address(pcvDeposit))); - assertTrue(!allocator.checkSkimCondition(address(pcvDeposit))); - } - - function testBufferUpdatesCorrectly() public { - vm.prank(addresses.governorAddress); - core.grantPCVController(address(allocator)); - - token.mint(address(pcvDeposit), targetBalance); - token.mint(address(psm), targetBalance / 2); - assertTrue(allocator.checkActionAllowed(address(pcvDeposit))); - assertTrue(allocator.checkDripCondition(address(pcvDeposit))); - assertTrue(!allocator.checkSkimCondition(address(pcvDeposit))); /// psm balance empty, cannot skim - - allocator.drip(address(pcvDeposit)); - - uint256 bufferEnd = gserl.buffer(); - token.mint(address(psm), targetBalance); - - allocator.skim(address(pcvDeposit)); - - uint256 bufferEndAfterSkim = gserl.buffer(); - assertEq(bufferEndAfterSkim, bufferCap); - assertEq(bufferEnd, bufferCap - targetBalance / 2); - } - - function testBufferDepletesAndReplenishesCorrectly() public { - vm.prank(addresses.governorAddress); - core.grantPCVController(address(allocator)); - - token.mint(address(pcvDeposit), targetBalance); - token.mint(address(psm), targetBalance / 2); - - assertTrue(allocator.checkActionAllowed(address(pcvDeposit))); - assertTrue(allocator.checkDripCondition(address(pcvDeposit))); - - allocator.drip(address(pcvDeposit)); - - uint256 bufferEnd = gserl.buffer(); - assertEq(bufferEnd, bufferCap - targetBalance / 2); - /// multiply by 2 to get over buffer cap and fully replenish buffer - token.mint(address(psm), (targetBalance * 3) / 2); - - assertTrue(allocator.checkSkimCondition(address(pcvDeposit))); - - allocator.skim(address(pcvDeposit)); - - uint256 bufferEndAfterSkim = gserl.buffer(); - assertEq(bufferEndAfterSkim, bufferCap); - } - - /// test a new deposit with decimal normalization - function testBufferDepletesAndReplenishesCorrectlyMultipleDecimalNormalizedDeposits() - public - { - vm.prank(addresses.governorAddress); - core.grantPCVController(address(allocator)); - - int8 decimalsNormalizer = 12; /// scale up new token by 12 decimals - uint256 scalingFactor = 1e12; /// scaling factor of 1e12 upwards - uint248 newTargetBalance = 100_000e6; /// target balance 100k - - MockERC20 newToken = new MockERC20(); - ERC20HoldingPCVDeposit newPsm = new ERC20HoldingPCVDeposit( - address(core), - IERC20(address(newToken)), - address(0) - ); - ERC20HoldingPCVDeposit newPcvDeposit = new ERC20HoldingPCVDeposit( - address(core), - IERC20(address(newToken)), - address(0) - ); - - vm.startPrank(addresses.governorAddress); - allocator.connectPSM( - address(newPsm), - newTargetBalance, - decimalsNormalizer - ); - allocator.connectDeposit(address(newPsm), address(newPcvDeposit)); - vm.stopPrank(); - - ( - address psmToken, - uint248 psmTargetBalance, - int8 _decimalsNormalizer - ) = allocator.allPSMs(address(newPsm)); - address _newPsm = allocator.pcvDepositToPSM(address(newPcvDeposit)); - - /// assert new PSM has been properly wired into the allocator - assertEq(psmTargetBalance, newTargetBalance); - assertEq(decimalsNormalizer, _decimalsNormalizer); - assertEq(psmToken, address(newToken)); - assertEq(address(_newPsm), address(newPsm)); - - assertTrue(!allocator.checkDripCondition(address(newPcvDeposit))); /// drip action not allowed, due to 0 balance - assertTrue(!allocator.checkDripCondition(address(pcvDeposit))); /// drip action not allowed, due to 0 balance - - { - ( - uint256 psmAmountToDrip, - uint256 psmAdjustedAmountToDrip - ) = allocator.getDripDetails(address(psm), address(pcvDeposit)); - - ( - uint256 newPsmAmountToDrip, - uint256 newPsmAdjustedAmountToDrip - ) = allocator.getDripDetails( - address(newPsm), - address(newPcvDeposit) - ); - - /// drips are 0 because pcv deposits are not funded - - assertEq(psmAmountToDrip, 0); - assertEq(newPsmAmountToDrip, 0); - - assertEq(psmAdjustedAmountToDrip, 0); - assertEq(newPsmAdjustedAmountToDrip, 0); - } - - token.mint(address(pcvDeposit), targetBalance); - newToken.mint(address(newPcvDeposit), newTargetBalance); - - { - ( - uint256 psmAmountToDrip, - uint256 psmAdjustedAmountToDrip - ) = allocator.getDripDetails(address(psm), address(pcvDeposit)); - - ( - uint256 newPsmAmountToDrip, - uint256 newPsmAdjustedAmountToDrip - ) = allocator.getDripDetails( - address(newPsm), - address(newPcvDeposit) - ); - - assertEq(psmAmountToDrip, targetBalance); - assertEq(newPsmAmountToDrip, newTargetBalance); - - assertEq(psmAdjustedAmountToDrip, targetBalance); - assertEq(newPsmAdjustedAmountToDrip, targetBalance); /// adjusted amount equals target balance - } - - assertTrue(allocator.checkActionAllowed(address(pcvDeposit))); - assertTrue(allocator.checkDripCondition(address(pcvDeposit))); /// drip action allowed, and balance to do it - assertTrue(!allocator.checkSkimCondition(address(pcvDeposit))); /// nothing to skim, balance is empty - - /// new PSM - assertTrue(allocator.checkActionAllowed(address(newPcvDeposit))); - assertTrue(allocator.checkDripCondition(address(newPcvDeposit))); /// drip action allowed, and balance to do it - assertTrue(!allocator.checkSkimCondition(address(newPcvDeposit))); /// nothing to skim, balance is empty - - { - uint256 startingBalancePcvDeposit = token.balanceOf( - address(pcvDeposit) - ); - uint256 startingBalanceNewPcvDeposit = newToken.balanceOf( - address(newPcvDeposit) - ); - - allocator.drip(address(pcvDeposit)); - allocator.drip(address(newPcvDeposit)); - - uint256 endingBalancePsm = token.balanceOf(address(psm)); - uint256 endingBalanceNewPsm = newToken.balanceOf(address(newPsm)); - uint256 endingBalancePcvDeposit = token.balanceOf( - address(pcvDeposit) - ); - uint256 endingBalanceNewPcvDeposit = newToken.balanceOf( - address(newPcvDeposit) - ); - - assertEq(startingBalancePcvDeposit, targetBalance); - assertEq(startingBalanceNewPcvDeposit, newTargetBalance); - - assertEq(endingBalancePsm, targetBalance); - assertEq(endingBalanceNewPsm, newTargetBalance); - - /// both of these should be zero'd out - assertEq(endingBalancePcvDeposit, 0); - assertEq(endingBalanceNewPcvDeposit, 0); - } - - assertEq(token.balanceOf(address(psm)), targetBalance); - assertEq(newToken.balanceOf(address(newPsm)), newTargetBalance); - - uint256 bufferEnd = gserl.buffer(); - assertEq(bufferEnd, bufferCap - targetBalance * 2); /// should have effectively dripped target balance 2x, meaning normalization worked properly - - /// multiply by 2 to get over buffer cap and fully replenish buffer - uint256 skimAmount = bufferCap - bufferEnd; - token.mint(address(psm), skimAmount); - newToken.mint(address(newPsm), skimAmount / scalingFactor); /// divide by scaling factor as new token only has 6 decimals - - { - (uint256 psmAmountToSkim, uint256 adjustedAmountToSkim) = allocator - .getSkimDetails(address(pcvDeposit)); - - assertEq(psmAmountToSkim, skimAmount); - assertEq(adjustedAmountToSkim, skimAmount); - - allocator.skim(address(pcvDeposit)); - } - - { - (uint256 psmAmountToSkim, uint256 adjustedAmountToSkim) = allocator - .getSkimDetails(address(newPcvDeposit)); - - assertEq(psmAmountToSkim, skimAmount / scalingFactor); /// actual amount is scaled up by 1e6 - assertEq(adjustedAmountToSkim, psmAmountToSkim * scalingFactor); /// adjusted amount is scaled up by 1e18 after scaling factor is applied - allocator.skim(address(newPcvDeposit)); - } - - assertEq(token.balanceOf(address(psm)), targetBalance); - assertEq(newToken.balanceOf(address(newPsm)), newTargetBalance); - - uint256 bufferEndAfterSkim = gserl.buffer(); - assertEq(bufferEndAfterSkim, bufferCap); /// fully replenish buffer, meaning normalization worked properly - } - - function testDripSucceedsWhenUnderFullTargetBalance( - uint8 denominator - ) public { - vm.assume(denominator > 1); - uint256 depositBalance = targetBalance / denominator; - - token.mint(address(pcvDeposit), depositBalance); - - vm.prank(addresses.governorAddress); - core.grantPCVController(address(allocator)); - - allocator.drip(address(pcvDeposit)); - - assertEq(token.balanceOf(address(pcvDeposit)), 0); - assertEq(token.balanceOf(address(psm)), depositBalance); - } - - function testSkimSucceedsWhenOverThresholdWithPCVControllerFuzz( - uint128 depositBalance - ) public { - vm.prank(addresses.governorAddress); - core.grantPCVController(address(allocator)); - token.mint(address(psm), depositBalance); - - if (depositBalance > targetBalance) { - assertTrue(allocator.checkSkimCondition(address(pcvDeposit))); - - allocator.skim(address(pcvDeposit)); - - assertTrue(!allocator.checkSkimCondition(address(pcvDeposit))); - assertEq(token.balanceOf(address(psm)), targetBalance); - assertEq( - token.balanceOf(address(pcvDeposit)), - depositBalance - targetBalance - ); - } else { - vm.expectRevert("ERC20Allocator: skim condition not met"); - allocator.skim(address(pcvDeposit)); - } - } - - function testDoActionDripSucceedsWhenUnderFullTargetBalance( - uint8 denominator - ) public { - vm.assume(denominator > 1); - uint256 depositBalance = targetBalance / denominator; - - token.mint(address(pcvDeposit), depositBalance); - - vm.prank(addresses.governorAddress); - core.grantPCVController(address(allocator)); - - uint256 bufferStart = gserl.buffer(); - (uint256 amountToDrip, uint256 adjustedAmountToDrip) = allocator - .getDripDetails(address(psm), address(pcvDeposit)); - - /// this has to be true - assertTrue(allocator.checkDripCondition(address(pcvDeposit))); - - allocator.doAction(address(pcvDeposit)); - - if (token.balanceOf(address(psm)) >= targetBalance) { - assertTrue(!allocator.checkDripCondition(address(pcvDeposit))); - } - - assertEq(bufferStart, gserl.buffer() + adjustedAmountToDrip); - assertEq(amountToDrip, adjustedAmountToDrip); - assertEq(token.balanceOf(address(pcvDeposit)), 0); - assertEq(token.balanceOf(address(psm)), depositBalance); - } - - function testDoActionSkimSucceedsWhenOverThresholdWithPCVControllerFuzz( - uint128 depositBalance - ) public { - vm.prank(addresses.governorAddress); - core.grantPCVController(address(allocator)); - token.mint(address(psm), depositBalance); - - if (depositBalance > targetBalance) { - uint256 bufferStart = gserl.buffer(); - allocator.doAction(address(pcvDeposit)); - - assertEq(bufferStart, gserl.buffer()); - assertEq(token.balanceOf(address(psm)), targetBalance); - assertEq( - token.balanceOf(address(pcvDeposit)), - depositBalance - targetBalance - ); - } - } - - /// tests where non whitelisted psm actions drip, skim and doAction fail - - function testDripFailsOnNonWhitelistedPSM() public { - address nonWhitelistedPSM = address(1); - ( - address psmToken, - uint248 psmTargetBalance, - int8 decimalsNormalizer - ) = allocator.allPSMs(nonWhitelistedPSM); - address psmPcvDeposit = allocator.pcvDepositToPSM(nonWhitelistedPSM); - - assertEq(psmTargetBalance, 0); - assertEq(decimalsNormalizer, 0); - assertEq(psmToken, address(0)); - assertEq(psmPcvDeposit, address(0)); - - /// points to token address 0, thus reverting in check drip condition - vm.expectRevert("ERC20Allocator: invalid PCVDeposit"); - allocator.drip(address(nonWhitelistedPSM)); - } - - function testSkimFailsOnNonWhitelistedPSM() public { - address nonWhitelistedPSM = address(1); - ( - address psmToken, - uint248 psmTargetBalance, - int8 decimalsNormalizer - ) = allocator.allPSMs(nonWhitelistedPSM); - address psmPcvDeposit = allocator.pcvDepositToPSM(nonWhitelistedPSM); - - assertEq(psmTargetBalance, 0); - assertEq(decimalsNormalizer, 0); - assertEq(psmToken, address(0)); - assertEq(psmPcvDeposit, address(0)); - - /// points to token address 0, thus reverting in check skim condition - vm.expectRevert("ERC20Allocator: invalid PCVDeposit"); - allocator.skim(address(nonWhitelistedPSM)); - } - - function testDoActionNoOpOnNonWhitelistedPSM() public { - address nonWhitelistedPSM = address(1); - ( - address psmToken, - uint248 psmTargetBalance, - int8 decimalsNormalizer - ) = allocator.allPSMs(nonWhitelistedPSM); - address psmPcvDeposit = allocator.pcvDepositToPSM(nonWhitelistedPSM); - - assertEq(psmTargetBalance, 0); - assertEq(decimalsNormalizer, 0); - assertEq(psmToken, address(0)); - assertEq(psmPcvDeposit, address(0)); - - /// points to token address 0, thus reverting in check drip condition - vm.expectRevert("ERC20Allocator: invalid PCVDeposit"); - allocator.doAction(address(nonWhitelistedPSM)); - } - - /// pure view only functions - - function testGetAdjustedAmountUp(uint128 amount) public { - int8 decimalsNormalizer = 18; /// add on 18 decimals - uint256 adjustedAmount = allocator.getAdjustedAmount( - amount, - decimalsNormalizer - ); - uint256 actualAmount = amount; /// cast up to avoid overflow - assertEq(adjustedAmount, actualAmount * 1e18); - } - - function testGetAdjustedAmountDown(uint128 amount) public { - int8 decimalsNormalizer = -18; /// remove 18 decimals - uint256 adjustedAmount = allocator.getAdjustedAmount( - amount, - decimalsNormalizer - ); - assertEq(adjustedAmount, amount / 1e18); - } -} diff --git a/test/unit/pcv/utils/ERC20AllocatorConnector.t.sol b/test/unit/pcv/utils/ERC20AllocatorConnector.t.sol deleted file mode 100644 index 7880987fc..000000000 --- a/test/unit/pcv/utils/ERC20AllocatorConnector.t.sol +++ /dev/null @@ -1,264 +0,0 @@ -pragma solidity =0.8.13; - -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -import {Vm} from "@forge-std/Vm.sol"; -import {CoreV2} from "@voltprotocol/core/CoreV2.sol"; -import {Test} from "@forge-std/Test.sol"; -import {MockPSM} from "@test/mock/MockPSM.sol"; -import {MockERC20} from "@test/mock/MockERC20.sol"; -import {getCoreV2} from "@test/unit/utils/Fixtures.sol"; -import {VoltRoles} from "@voltprotocol/core/VoltRoles.sol"; -import {PCVDeposit} from "@voltprotocol/pcv/PCVDeposit.sol"; -import {ERC20Allocator} from "@voltprotocol/pcv/ERC20Allocator.sol"; -import {ERC20HoldingPCVDeposit} from "@test/mock/ERC20HoldingPCVDeposit.sol"; -import {TestAddresses as addresses} from "@test/unit/utils/TestAddresses.sol"; -import {IGlobalReentrancyLock, GlobalReentrancyLock} from "@voltprotocol/core/GlobalReentrancyLock.sol"; -import {IGlobalSystemExitRateLimiter, GlobalSystemExitRateLimiter} from "@voltprotocol/rate-limits/GlobalSystemExitRateLimiter.sol"; - -contract UnitTestERC20AllocatorConnector is Test { - /// @notice emitted when an existing deposit is updated - event DepositUpdated( - address psm, - address pcvDeposit, - address token, - uint248 targetBalance, - int8 decimalsNormalizer - ); - - /// @notice PSM deletion event - event PSMDeleted(address psm); - - /// @notice event emitted when tokens are dripped - event Dripped(uint256 amount); - - /// @notice event emitted in do action when neither skim nor drip could be triggered - event NoOp(); - - /// @notice emitted when an existing deposit is deleted - event DepositDeleted(address psm); - - /// @notice emitted when a psm is connected to a PCV Deposit - event DepositConnected(address psm, address pcvDeposit); - - CoreV2 private core; - - /// @notice reference to the PCVDeposit to pull from - ERC20HoldingPCVDeposit private pcvDeposit; - - /// @notice reference to the PCVDeposit to push to - ERC20HoldingPCVDeposit private psm; - - /// @notice reference to the ERC20 - ERC20Allocator private allocator; - - /// @notice reference to global system exit rate limiter - GlobalSystemExitRateLimiter private gserl; - - /// @notice token to push - MockERC20 private token; - - /// @notice global reentrancy lock - IGlobalReentrancyLock private lock; - - /// @notice threshold over which to pull tokens from pull deposit - uint248 private constant targetBalance = 100_000e18; - - /// @notice maximum rate limit per second in RateLimitedV2 - uint256 private constant maxRateLimitPerSecond = 1_000_000e18; - - /// @notice rate limit per second in RateLimitedV2 - uint128 private constant rateLimitPerSecond = 10_000e18; - - /// @notice buffer cap in RateLimitedV2 - uint128 private constant bufferCap = 10_000_000e18; - - function setUp() public { - core = getCoreV2(); - token = new MockERC20(); - lock = IGlobalReentrancyLock( - address(new GlobalReentrancyLock(address(core))) - ); - - pcvDeposit = new ERC20HoldingPCVDeposit( - address(core), - IERC20(address(token)), - address(0) - ); - - psm = new ERC20HoldingPCVDeposit( - address(core), - IERC20(address(token)), - address(0) - ); - - gserl = new GlobalSystemExitRateLimiter( - address(core), - maxRateLimitPerSecond, - rateLimitPerSecond, - bufferCap - ); - - allocator = new ERC20Allocator(address(core)); - - vm.startPrank(addresses.governorAddress); - - allocator.connectPSM(address(psm), targetBalance, 0); - allocator.connectDeposit(address(psm), address(pcvDeposit)); - - core.grantLocker(address(allocator)); - core.grantLocker(address(gserl)); - core.grantSystemExitRateLimitDepleter(address(allocator)); - core.grantSystemExitRateLimitReplenisher(address(allocator)); - - core.setGlobalSystemExitRateLimiter( - IGlobalSystemExitRateLimiter(address(gserl)) - ); - core.setGlobalReentrancyLock(lock); - - vm.stopPrank(); - } - - function testSkimFailsToNonConnectedAddress(address deposit) public { - vm.assume(deposit != address(pcvDeposit)); - vm.expectRevert("ERC20Allocator: invalid PCVDeposit"); - allocator.skim(address(deposit)); - } - - function testDripFailsToNonConnectedAddress(address deposit) public { - vm.assume(deposit != address(pcvDeposit)); - vm.expectRevert("ERC20Allocator: invalid PCVDeposit"); - allocator.drip(address(deposit)); - } - - function testConnectNewDepositSkimToDripFrom() public { - PCVDeposit newPcvDeposit = new ERC20HoldingPCVDeposit( - address(core), - IERC20(address(token)), - address(0) - ); - - vm.startPrank(addresses.governorAddress); - allocator.connectDeposit(address(psm), address(newPcvDeposit)); - core.grantPCVController(address(allocator)); - vm.stopPrank(); - - assertEq( - address(psm), - allocator.pcvDepositToPSM(address(newPcvDeposit)) - ); - assertEq(psm.balance(), 0); - - /// drip - token.mint(address(newPcvDeposit), targetBalance); - allocator.drip(address(newPcvDeposit)); - - assertEq(psm.balance(), targetBalance); - assertEq(newPcvDeposit.balance(), 0); - - /// skim - token.mint(address(psm), targetBalance); - - assertEq(psm.balance(), targetBalance * 2); - - allocator.skim(address(newPcvDeposit)); - - assertEq(psm.balance(), targetBalance); - assertEq(newPcvDeposit.balance(), targetBalance); - } - - function testConnectNewDepositFailsTokenMismatch() public { - PCVDeposit newPcvDeposit = new ERC20HoldingPCVDeposit( - address(core), - IERC20(address(0)), - address(0) - ); - - vm.prank(addresses.governorAddress); - vm.expectRevert("ERC20Allocator: token mismatch"); - allocator.connectDeposit(address(psm), address(newPcvDeposit)); - } - - function testConnectAndRemoveNewDeposit() public { - PCVDeposit newPcvDeposit = new ERC20HoldingPCVDeposit( - address(core), - IERC20(address(token)), - address(0) - ); - - vm.prank(addresses.governorAddress); - allocator.connectDeposit(address(psm), address(newPcvDeposit)); - - assertEq( - allocator.pcvDepositToPSM(address(newPcvDeposit)), - address(psm) - ); - - vm.prank(addresses.governorAddress); - allocator.deleteDeposit(address(newPcvDeposit)); - - assertEq(allocator.pcvDepositToPSM(address(newPcvDeposit)), address(0)); - - vm.expectRevert("ERC20Allocator: invalid PCVDeposit"); - allocator.drip(address(newPcvDeposit)); - - vm.expectRevert("ERC20Allocator: invalid PCVDeposit"); - allocator.skim(address(newPcvDeposit)); - } - - function testCreateNewDepositFailsUnderlyingTokenMismatch() public { - PCVDeposit newPcvDeposit = new ERC20HoldingPCVDeposit( - address(core), - IERC20(address(1)), - address(0) - ); - - ERC20HoldingPCVDeposit newPsm = new ERC20HoldingPCVDeposit( - address(core), - IERC20(address(token)), - address(0) - ); - - vm.startPrank(addresses.governorAddress); - allocator.connectPSM(address(newPsm), 0, 0); - vm.expectRevert("ERC20Allocator: token mismatch"); - allocator.connectDeposit(address(newPsm), address(newPcvDeposit)); - vm.stopPrank(); - } - - function testConnectNewDepositFailsUnderlyingTokenMismatch() public { - PCVDeposit newPcvDeposit = new ERC20HoldingPCVDeposit( - address(core), - IERC20(address(1)), - address(0) - ); - - vm.expectRevert("ERC20Allocator: token mismatch"); - vm.prank(addresses.governorAddress); - allocator.connectDeposit(address(psm), address(newPcvDeposit)); - } - - function testEditPSMTargetBalanceFailsPsmUnderlyingChanged() public { - MockPSM newPsm = new MockPSM(address(token)); - vm.prank(addresses.governorAddress); - allocator.connectPSM(address(newPsm), targetBalance, 0); - newPsm.setUnderlying(address(1)); - - vm.expectRevert("ERC20Allocator: psm changed underlying"); - vm.prank(addresses.governorAddress); - allocator.editPSMTargetBalance(address(newPsm), 0); - } - - function testCreateDuplicateDepositFails() public { - vm.expectRevert("ERC20Allocator: cannot overwrite existing deposit"); - vm.prank(addresses.governorAddress); - allocator.connectPSM(address(psm), targetBalance, 0); - } - - function testSetTargetBalanceNonExistingPsmFails() public { - MockPSM newPsm = new MockPSM(address(token)); - vm.expectRevert("ERC20Allocator: cannot edit non-existent deposit"); - vm.prank(addresses.governorAddress); - allocator.editPSMTargetBalance(address(newPsm), targetBalance); - } -} diff --git a/test/unit/peg/UnitTestNonCustodialPSM.t.sol b/test/unit/peg/UnitTestNonCustodialPSM.t.sol index c89898512..4c66b0b51 100644 --- a/test/unit/peg/UnitTestNonCustodialPSM.t.sol +++ b/test/unit/peg/UnitTestNonCustodialPSM.t.sol @@ -18,12 +18,10 @@ import {IPCVDepositV2} from "@voltprotocol/pcv/IPCVDepositV2.sol"; import {NonCustodialPSM} from "@voltprotocol/peg/NonCustodialPSM.sol"; import {MockPCVDepositV3} from "@test/mock/MockPCVDepositV3.sol"; import {VoltSystemOracle} from "@voltprotocol/oracle/VoltSystemOracle.sol"; -import {PegStabilityModule} from "@voltprotocol/peg/PegStabilityModule.sol"; import {TestAddresses as addresses} from "@test/unit/utils/TestAddresses.sol"; import {getCoreV2, getVoltSystemOracle} from "@test/unit/utils/Fixtures.sol"; import {IGlobalReentrancyLock, GlobalReentrancyLock} from "@voltprotocol/core/GlobalReentrancyLock.sol"; import {IGlobalRateLimitedMinter, GlobalRateLimitedMinter} from "@voltprotocol/rate-limits/GlobalRateLimitedMinter.sol"; -import {IGlobalSystemExitRateLimiter, GlobalSystemExitRateLimiter} from "@voltprotocol/rate-limits/GlobalSystemExitRateLimiter.sol"; /// deployment steps /// 1. core v2 @@ -68,9 +66,7 @@ contract NonCustodialPSMUnitTest is Test { NonCustodialPSM private psm; VoltSystemOracle private oracle; GlobalRateLimitedMinter private grlm; - GlobalSystemExitRateLimiter private gserl; MockPCVDepositV3 private pcvDeposit; - PegStabilityModule private custodialPsm; IGlobalReentrancyLock private lock; address private voltAddress; @@ -88,10 +84,10 @@ contract NonCustodialPSMUnitTest is Test { uint256 public constant maxRateLimitPerSecondMinting = 100e18; /// replenish 500k VOLT per day - uint128 public constant rateLimitPerSecondMinting = 5.787e18; + uint64 public constant rateLimitPerSecondMinting = 5.787e18; /// buffer cap of 1.5m VOLT - uint128 public constant bufferCapMinting = 1_500_000e18; + uint96 public constant bufferCapMinting = 1_500_000e18; /// ---------- ALLOCATOR PARAMS ---------- @@ -148,53 +144,27 @@ contract NonCustodialPSMUnitTest is Test { IPCVDepositV2(address(pcvDeposit)) ); - custodialPsm = new PegStabilityModule( - coreAddress, - address(oracle), - address(0), - 0, - false, - dai, - voltFloorPrice, - voltCeilingPrice - ); - - gserl = new GlobalSystemExitRateLimiter( - coreAddress, - maxRateLimitPerSecond, - rateLimitPerSecond, - bufferCap - ); - vm.startPrank(addresses.governorAddress); core.grantPCVController(address(psm)); core.grantMinter(address(grlm)); - core.grantSystemExitRateLimitDepleter(address(psm)); - core.grantRateLimitedRedeemer(address(psm)); - core.grantRateLimitedRedeemer(address(custodialPsm)); + core.grantPsmMinter(address(psm)); core.grantLocker(address(entry)); core.grantLocker(address(psm)); - core.grantLocker(address(custodialPsm)); core.grantLocker(address(pcvDeposit)); core.grantLocker(address(grlm)); - core.grantLocker(address(gserl)); core.setGlobalRateLimitedMinter( IGlobalRateLimitedMinter(address(grlm)) ); - core.setGlobalSystemExitRateLimiter( - IGlobalSystemExitRateLimiter(address(gserl)) - ); core.setGlobalReentrancyLock(lock); vm.stopPrank(); /// top up contracts with tokens for testing dai.mint(address(pcvDeposit), daiTargetBalance); - dai.mint(address(custodialPsm), daiTargetBalance); entry.deposit(address(pcvDeposit)); vm.label(address(psm), "psm"); @@ -206,8 +176,7 @@ contract NonCustodialPSMUnitTest is Test { assertTrue(core.isLocker(address(psm))); assertTrue(core.isMinter(address(grlm))); - assertTrue(!core.isRateLimitedMinter(address(psm))); - assertTrue(core.isRateLimitedRedeemer(address(psm))); + assertTrue(core.isPsmMinter(address(psm))); assertTrue(core.isPCVController(address(psm))); assertEq(address(core.globalRateLimitedMinter()), address(grlm)); @@ -218,18 +187,10 @@ contract NonCustodialPSMUnitTest is Test { assertEq(address(psm.pcvDeposit()), address(pcvDeposit)); assertEq(psm.decimalsNormalizer(), 0); assertEq(address(psm.underlyingToken()), address(dai)); - - assertEq( - address(psm.underlyingToken()), - address(custodialPsm.underlyingToken()) - ); - assertEq(psm.decimalsNormalizer(), custodialPsm.decimalsNormalizer()); - assertEq(psm.ceiling(), custodialPsm.ceiling()); - assertEq(psm.floor(), custodialPsm.floor()); } function testGetMaxRedeemAmountIn() public { - uint256 buffer = gserl.buffer(); + uint256 buffer = grlm.buffer(); uint256 oraclePrice = psm.readOracle(); uint256 pcvDepositBalance = pcvDeposit.balance(); @@ -328,10 +289,6 @@ contract NonCustodialPSMUnitTest is Test { amountOut.toInt256(), 0 ); - assertEq( - psm.getRedeemAmountOut(amountVoltIn).toInt256(), - custodialPsm.getRedeemAmountOut(amountVoltIn).toInt256() - ); assertApproxEqPpq( psm.getRedeemAmountOut(amountVoltIn).toInt256(), amountOut.toInt256(), @@ -343,7 +300,7 @@ contract NonCustodialPSMUnitTest is Test { vm.assume(redeemAmount != 0); uint256 amountOut; - { + uint256 voltBalance = volt.balanceOf(address(this)); uint256 underlyingAmountOut = psm.getRedeemAmountOut(voltBalance); uint256 userStartingUnderlyingBalance = dai.balanceOf( @@ -357,7 +314,7 @@ contract NonCustodialPSMUnitTest is Test { underlyingAmountOut, psm.redeem(address(this), voltBalance, underlyingAmountOut) ); - assertEq(bufferCap - underlyingAmountOut, gserl.buffer()); + assertEq(bufferCap - underlyingAmountOut, grlm.buffer()); uint256 depositEndingUnderlyingBalance = pcvDeposit.balance(); uint256 userEndingUnderlyingBalance = dai.balanceOf(address(this)); @@ -380,54 +337,6 @@ contract NonCustodialPSMUnitTest is Test { userStartingUnderlyingBalance ); assertEq(volt.balanceOf(address(psm)), 0); - } - { - uint256 voltBalance = volt.balanceOf(address(this)); - uint256 underlyingAmountOut = custodialPsm.getRedeemAmountOut( - voltBalance - ); - uint256 userStartingUnderlyingBalance = dai.balanceOf( - address(this) - ); - uint256 depositStartingUnderlyingBalance = custodialPsm.balance(); - - volt.approve(address(psm), voltBalance); - assertEq( - underlyingAmountOut, - custodialPsm.redeem( - address(this), - voltBalance, - underlyingAmountOut - ) - ); - - uint256 depositEndingUnderlyingBalance = custodialPsm.balance(); - uint256 userEndingUnderlyingBalance = dai.balanceOf(address(this)); - uint256 bufferAfterRedeem = grlm.buffer(); - - uint256 endingBalance = dai.balanceOf(address(this)); - - assertEq(endingBalance, underlyingAmountOut); - - assertEq( - depositStartingUnderlyingBalance - - depositEndingUnderlyingBalance, - underlyingAmountOut - ); - - assertEq(bufferAfterRedeem, grlm.bufferCap()); - assertEq(bufferAfterRedeem, grlm.buffer()); - assertEq( - userEndingUnderlyingBalance - underlyingAmountOut, - userStartingUnderlyingBalance - ); - assertEq( - userEndingUnderlyingBalance - userStartingUnderlyingBalance, - amountOut - ); - assertEq(volt.balanceOf(address(psm)), 0); - assertEq(amountOut, underlyingAmountOut); - } } function testRedeemDifferentialSucceeds(uint128 redeemAmount) public { @@ -457,7 +366,7 @@ contract NonCustodialPSMUnitTest is Test { underlyingAmountOut ); - assertEq(bufferCap - underlyingAmountOut, gserl.buffer()); + assertEq(bufferCap - underlyingAmountOut, grlm.buffer()); assertEq(bufferAfterRedeem, grlm.bufferCap()); assertEq(bufferAfterRedeem, grlm.buffer()); diff --git a/test/unit/peg/UnitTestPegStabilityModule.t.sol b/test/unit/peg/UnitTestPegStabilityModule.t.sol deleted file mode 100644 index 2c3fa1755..000000000 --- a/test/unit/peg/UnitTestPegStabilityModule.t.sol +++ /dev/null @@ -1,517 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.13; - -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; - -import {Vm} from "@forge-std/Vm.sol"; -import {CoreV2} from "@voltprotocol/core/CoreV2.sol"; -import {ICoreV2} from "@voltprotocol/core/ICoreV2.sol"; -import {Constants} from "@voltprotocol/Constants.sol"; -import {MockERC20} from "@test/mock/MockERC20.sol"; -import {IVolt, Volt} from "@voltprotocol/v1/Volt.sol"; -import {PCVGuardian} from "@voltprotocol/pcv/PCVGuardian.sol"; -import {SystemEntry} from "@voltprotocol/entry/SystemEntry.sol"; -import {Test} from "@forge-std/Test.sol"; -import {NonCustodialPSM} from "@voltprotocol/peg/NonCustodialPSM.sol"; -import {VoltSystemOracle} from "@voltprotocol/oracle/VoltSystemOracle.sol"; -import {PegStabilityModule} from "@voltprotocol/peg/PegStabilityModule.sol"; -import {TestAddresses as addresses} from "@test/unit/utils/TestAddresses.sol"; -import {getCoreV2, getLocalOracleSystem} from "@test/unit/utils/Fixtures.sol"; -import {IGlobalReentrancyLock, GlobalReentrancyLock} from "@voltprotocol/core/GlobalReentrancyLock.sol"; -import {IGlobalRateLimitedMinter, GlobalRateLimitedMinter} from "@voltprotocol/rate-limits/GlobalRateLimitedMinter.sol"; - -/// PSM Unit Test that tests new PSM to ensure proper behavior -contract UnitTestPegStabilityModule is Test { - using SafeCast for *; - - /// @notice PSM to test against - PegStabilityModule private psm; - - IVolt private volt; - ICoreV2 private core; - SystemEntry private entry; - IERC20 private underlyingToken; - PCVGuardian private pcvGuardian; - VoltSystemOracle private oracle; - IGlobalReentrancyLock private lock; - GlobalRateLimitedMinter private grlm; - - uint256 public constant mintAmount = 10_000_000e18; - uint256 public constant voltMintAmount = 10_000_000e18; - - /// ---------- PRICE PARAMS ---------- - - uint128 voltFloorPrice = 1.04e18; - - uint128 voltCeilingPrice = 1.1e18; - - /// ---------- GRLM PARAMS ---------- - - /// maximum rate limit per second is 100 VOLT - uint256 public constant maxRateLimitPerSecondMinting = 100e18; - - /// replenish 500k VOLT per day - uint128 public constant rateLimitPerSecondMinting = 5.787e18; - - /// buffer cap of 10m VOLT - uint128 public constant bufferCapMinting = uint128(voltMintAmount); - - function setUp() public { - underlyingToken = IERC20(address(new MockERC20())); - core = getCoreV2(); - volt = core.volt(); - oracle = getLocalOracleSystem(address(core), uint112(voltFloorPrice)); - lock = IGlobalReentrancyLock( - address(new GlobalReentrancyLock(address(core))) - ); - - /// create PSM - psm = new PegStabilityModule( - address(core), - address(oracle), - address(0), - 0, - false, - underlyingToken, - voltFloorPrice, - voltCeilingPrice - ); - - vm.prank(addresses.governorAddress); - grlm = new GlobalRateLimitedMinter( - address(core), - maxRateLimitPerSecondMinting, - rateLimitPerSecondMinting, - bufferCapMinting - ); - entry = new SystemEntry(address(core)); - - address[] memory toWhitelist = new address[](1); - toWhitelist[0] = address(psm); - - pcvGuardian = new PCVGuardian( - address(core), - address(this), - toWhitelist - ); - - vm.startPrank(addresses.governorAddress); - - core.setGlobalRateLimitedMinter( - IGlobalRateLimitedMinter(address(grlm)) - ); - core.setGlobalReentrancyLock(lock); - - core.grantPCVController(address(pcvGuardian)); - - core.grantMinter(address(grlm)); - - core.grantRateLimitedRedeemer(address(psm)); - core.grantRateLimitedMinter(address(psm)); - - core.grantGuardian(address(pcvGuardian)); - - core.grantPCVGuard(address(this)); - - core.grantLocker(address(psm)); - core.grantLocker(address(grlm)); - core.grantLocker(address(entry)); - core.grantLocker(address(pcvGuardian)); - - vm.stopPrank(); - - vm.label(address(psm), "PSM"); - vm.label(address(grlm), "GRLM"); - vm.label(address(core), "CORE"); - - /// mint VOLT to the user - volt.mint(address(this), voltMintAmount); - - deal(address(underlyingToken), address(psm), mintAmount); - deal(address(underlyingToken), address(this), mintAmount); - } - - /// @notice PSM is set up correctly - function testSetUpCorrectly() public { - assertTrue(!psm.doInvert()); - assertEq(address(psm.oracle()), address(oracle)); - assertEq(address(psm.backupOracle()), address(0)); - assertEq(psm.decimalsNormalizer(), 0); - assertEq(address(psm.underlyingToken()), address(underlyingToken)); - assertEq(psm.floor(), voltFloorPrice); - assertEq(psm.ceiling(), voltCeilingPrice); - } - - /// @notice PSM is set up correctly and redeem view function is working - function testGetRedeemAmountOut(uint128 amountVoltIn) public { - uint256 currentPegPrice = oracle.getCurrentOraclePrice(); - uint256 amountOut = (amountVoltIn * currentPegPrice) / 1e18; - - assertApproxEq( - psm.getRedeemAmountOut(amountVoltIn).toInt256(), - amountOut.toInt256(), - 0 - ); - } - - /// @notice PSM is set up correctly and redeem view function is working - function testGetRedeemAmountOutPpq(uint128 amountVoltIn) public { - vm.assume(amountVoltIn > 1e8); - uint256 currentPegPrice = oracle.getCurrentOraclePrice(); - uint256 amountOut = (amountVoltIn * currentPegPrice) / 1e18; - - assertApproxEq( - psm.getRedeemAmountOut(amountVoltIn).toInt256(), - amountOut.toInt256(), - 0 - ); - assertApproxEqPpq( - psm.getRedeemAmountOut(amountVoltIn).toInt256(), - amountOut.toInt256(), - 1_000_000_000 - ); - } - - /// @notice PSM is set up correctly and view functions are working - function testGetMintAmountOut(uint128 amountDaiIn) public { - uint256 currentPegPrice = oracle.getCurrentOraclePrice(); - uint256 amountOut = (uint256(amountDaiIn) * 1e18) / currentPegPrice; - assertApproxEq( - psm.getMintAmountOut(amountDaiIn).toInt256(), - amountOut.toInt256(), - 0 - ); - } - - /// @notice PSM is set up correctly and view functions are working - function testGetMintAmountOutPpq(uint128 amountDaiIn) public { - vm.assume(amountDaiIn > 1e8); - uint256 currentPegPrice = oracle.getCurrentOraclePrice(); - uint256 amountOut = (uint256(amountDaiIn) * 1e18) / currentPegPrice; - assertApproxEq( - psm.getMintAmountOut(amountDaiIn).toInt256(), - amountOut.toInt256(), - 0 - ); - } - - function testMintFuzz(uint72 amountStableIn) public { - uint256 amountVoltOut = psm.getMintAmountOut(amountStableIn); - uint256 startingVoltTotalSupply = volt.totalSupply(); - uint256 startingpsmBalance = psm.balance(); - uint256 startingUserVoltBalance = volt.balanceOf(address(this)); - uint256 startingBuffer = grlm.buffer(); - - underlyingToken.approve(address(psm), amountStableIn); - psm.mint(address(this), amountStableIn, amountVoltOut); - - uint256 endingVoltTotalSupply = volt.totalSupply(); - uint256 endingUserVoltBalance = volt.balanceOf(address(this)); - uint256 endingpsmBalance = psm.balance(); - uint256 endingBuffer = grlm.buffer(); - - assertEq(startingBuffer - endingBuffer, amountVoltOut); - - assertEq( - startingVoltTotalSupply + amountVoltOut, - endingVoltTotalSupply - ); - - assertEq( - endingUserVoltBalance - amountVoltOut, - startingUserVoltBalance - ); - - /// assert psm receives amount stable in - assertEq(endingpsmBalance - startingpsmBalance, amountStableIn); - } - - function testGetMaxMintAmountOut() public { - uint256 maxVoltAmountOut = psm.getMaxMintAmountOut(); - - assertEq(grlm.buffer(), maxVoltAmountOut); - } - - function testGetMaxRedeemAmountOut() public { - uint256 maxVoltAmountRedeemed = psm.getMaxRedeemAmountIn(); - uint256 balance = underlyingToken.balanceOf(address(psm)); - uint256 oraclePrice = psm.readOracle(); - - assertEq((balance * 1e18) / oraclePrice, maxVoltAmountRedeemed); - } - - function testMintFuzzNotEnoughIn(uint32 amountStableIn) public { - uint256 amountVoltOut = psm.getMintAmountOut(amountStableIn); - underlyingToken.approve(address(psm), amountStableIn); - vm.expectRevert("PegStabilityModule: Mint not enough out"); - psm.mint(address(this), amountStableIn, amountVoltOut + 1); - } - - function testRedeemFuzz(uint72 amountVoltIn) public { - uint256 amountOut = psm.getRedeemAmountOut(amountVoltIn); - - uint256 currentPegPrice = oracle.getCurrentOraclePrice(); - uint256 underlyingOutOracleAmount = (amountVoltIn * currentPegPrice) / - 1e18; - - uint256 startingUserUnderlyingBalance = underlyingToken.balanceOf( - address(this) - ); - uint256 startingPsmUnderlyingBalance = psm.balance(); - uint256 startingUserVoltBalance = volt.balanceOf(address(this)); - - volt.approve(address(psm), amountVoltIn); - - uint256 g0 = gasleft(); - psm.redeem(address(this), amountVoltIn, amountOut); - uint256 g1 = gasleft(); - - emit log_named_uint("cost per redeem: 1", (g0 - g1)); - - uint256 endingUserUnderlyingBalance1 = underlyingToken.balanceOf( - address(this) - ); - - uint256 endingUserVoltBalance = volt.balanceOf(address(this)); - uint256 endingPsmUnderlyingBalance = psm.balance(); - assertEq( - startingPsmUnderlyingBalance - endingPsmUnderlyingBalance, - amountOut - ); - assertEq(startingUserVoltBalance - endingUserVoltBalance, amountVoltIn); - assertEq( - endingUserUnderlyingBalance1, - startingUserUnderlyingBalance + amountOut - ); - assertApproxEq( - underlyingOutOracleAmount.toInt256(), - amountOut.toInt256(), - 0 - ); - } - - function testRedeem() public { - uint72 amountVoltIn = 1_000e18; - uint256 amountOut = psm.getRedeemAmountOut(amountVoltIn); - - uint256 currentPegPrice = oracle.getCurrentOraclePrice(); - uint256 underlyingOutOracleAmount = (amountVoltIn * currentPegPrice) / - 1e18; - - uint256 startingUserUnderlyingBalance = underlyingToken.balanceOf( - address(this) - ); - uint256 startingPsmUnderlyingBalance = psm.balance(); - uint256 startingUserVoltBalance = volt.balanceOf(address(this)); - - volt.approve(address(psm), amountVoltIn); - - uint256 g0 = gasleft(); - psm.redeem(address(this), amountVoltIn, amountOut); - uint256 g1 = gasleft(); - - emit log_named_uint("cost per redeem: 1", (g0 - g1)); - - uint256 endingUserUnderlyingBalance1 = underlyingToken.balanceOf( - address(this) - ); - - uint256 endingUserVoltBalance = volt.balanceOf(address(this)); - uint256 endingPsmUnderlyingBalance = psm.balance(); - assertEq( - startingPsmUnderlyingBalance - endingPsmUnderlyingBalance, - amountOut - ); - assertEq(startingUserVoltBalance - endingUserVoltBalance, amountVoltIn); - assertEq( - endingUserUnderlyingBalance1, - startingUserUnderlyingBalance + amountOut - ); - assertApproxEq( - underlyingOutOracleAmount.toInt256(), - amountOut.toInt256(), - 0 - ); - } - - function testRedeemFuzzNotEnoughOut(uint96 amountVoltIn) public { - uint256 amountOut = psm.getRedeemAmountOut(amountVoltIn); - - volt.approve(address(psm), amountVoltIn); - vm.expectRevert("PegStabilityModule: Redeem not enough out"); - psm.redeem(address(this), amountVoltIn, amountOut + 1); - } - - /// @notice pcv deposit receives underlying token on mint - function testSwapUnderlyingForVolt() public { - uint256 amountStableIn = 101_000; - uint256 amountVoltOut = psm.getMintAmountOut(amountStableIn); - uint256 startingUserVoltBalance = volt.balanceOf(address(this)); - uint256 startingPSMUnderlyingBalance = underlyingToken.balanceOf( - address(psm) - ); - underlyingToken.approve(address(psm), amountStableIn); - - uint256 g0 = gasleft(); - psm.mint(address(this), amountStableIn, amountVoltOut); - uint256 g1 = gasleft(); - - emit log_named_uint("cost per mint: 1", (g0 - g1)); - - uint256 endingUserVoltBalance = volt.balanceOf(address(this)); - uint256 endingPSMUnderlyingBalance = underlyingToken.balanceOf( - address(psm) - ); - assertEq( - endingUserVoltBalance, - startingUserVoltBalance + amountVoltOut - ); - assertEq( - startingPSMUnderlyingBalance + amountStableIn, - endingPSMUnderlyingBalance - ); - } - - /// @notice redeem fails without approval - function testSwapVoltForunderlyingTokenFailsWithoutApproval() public { - vm.expectRevert("ERC20: insufficient allowance"); - psm.redeem(address(this), mintAmount, mintAmount); - } - - function testMintFailsWhenMintExceedsBuffer() public { - underlyingToken.approve(address(psm), type(uint256).max); - uint256 currentPegPrice = oracle.getCurrentOraclePrice(); - uint256 psmVoltBalance = grlm.buffer() + 1; /// try to mint 1 wei over buffer which causes failure - /// we get the amount we want to put in by getting the - /// total PSM balance and dividing by the current peg price - /// this lets us get the maximum amount we can deposit - uint256 amountIn = (psmVoltBalance * currentPegPrice) / 1e6; - // this will revert (correctly) as the math above is less precise than the PSMs, therefore our amountIn - // will slightly exceed the balance the PSM can give to us. - vm.expectRevert("RateLimited: rate limit hit"); - psm.mint(address(this), amountIn, psmVoltBalance); - } - - /// @notice mint fails without approval - function testSwapUnderlyingForVoltFailsWithoutApproval() public { - vm.expectRevert("ERC20: insufficient allowance"); - psm.mint(address(this), mintAmount, 0); - } - - /// @notice withdraw succeeds with correct permissions - function testWithdrawSuccess() public { - uint256 startingBalance = underlyingToken.balanceOf(address(this)); - pcvGuardian.withdrawToSafeAddress(address(psm), mintAmount); - uint256 endingBalance = underlyingToken.balanceOf(address(this)); - assertEq(endingBalance - startingBalance, mintAmount); - } - - function testSetOracleFloorPriceGovernorSucceeds() public { - uint128 currentPrice = uint128(oracle.getCurrentOraclePrice()); - vm.prank(addresses.governorAddress); - psm.setOracleFloorPrice(currentPrice); - assertTrue(psm.isPriceValid()); - } - - function testSetOracleCeilingPriceGovernorSucceeds() public { - uint128 currentPrice = uint128(oracle.getCurrentOraclePrice()); - vm.prank(addresses.governorAddress); - psm.setOracleCeilingPrice(currentPrice + 1); - assertTrue(psm.isPriceValid()); - } - - function testSetOracleCeilingPriceGovernorLteFloorFails() public { - uint128 currentFloor = psm.floor(); - - vm.startPrank(addresses.governorAddress); - - vm.expectRevert( - "PegStabilityModule: ceiling must be greater than floor" - ); - psm.setOracleCeilingPrice(currentFloor); - - vm.expectRevert( - "PegStabilityModule: ceiling must be greater than floor" - ); - psm.setOracleCeilingPrice(currentFloor - 1); - - vm.stopPrank(); - } - - function testSetOracleFloorPrice0GovernorFails() public { - vm.expectRevert("PegStabilityModule: invalid floor"); - vm.prank(addresses.governorAddress); - psm.setOracleFloorPrice(0); - } - - function testSetOracleFloorPriceGovernorSucceedsFuzz( - uint128 newFloorPrice - ) public { - vm.assume(newFloorPrice != 0); - - uint128 currentPrice = uint128(oracle.getCurrentOraclePrice()); - uint128 currentFloor = psm.floor(); - uint128 currentCeiling = psm.ceiling(); - - if (newFloorPrice < currentFloor) { - vm.prank(addresses.governorAddress); - psm.setOracleFloorPrice(newFloorPrice); - assertTrue(psm.isPriceValid()); - testMintFuzz(100_000); - testRedeemFuzz(100_000); - } else if (newFloorPrice >= currentCeiling) { - vm.expectRevert( - "PegStabilityModule: floor must be less than ceiling" - ); - vm.prank(addresses.governorAddress); - psm.setOracleFloorPrice(newFloorPrice); - assertTrue(psm.isPriceValid()); - testMintFuzz(100_000); - testRedeemFuzz(100_000); - } else if (newFloorPrice > currentPrice) { - vm.prank(addresses.governorAddress); - psm.setOracleFloorPrice(newFloorPrice); - assertTrue(!psm.isPriceValid()); - - vm.expectRevert("PegStabilityModule: price out of bounds"); - psm.mint(address(this), 1, 0); - vm.expectRevert("PegStabilityModule: price out of bounds"); - psm.redeem(address(this), 1, 0); - } - } - - /// @notice withdraw erc20 succeeds with correct permissions - function testERC20WithdrawSuccess() public { - vm.prank(addresses.governorAddress); - core.grantPCVController(address(this)); - uint256 startingBalance = underlyingToken.balanceOf(address(this)); - psm.withdrawERC20(address(underlyingToken), address(this), mintAmount); - uint256 endingBalance = underlyingToken.balanceOf(address(this)); - assertEq(endingBalance - startingBalance, mintAmount); - } - - /// ----------- ACL TESTS ----------- - - /// @notice withdraw fails without correct permissions - function testWithdrawFailure() public { - vm.expectRevert(bytes("CoreRef: Caller is not a PCV controller")); - psm.withdraw(address(this), 100); - } - - /// @notice withdraw erc20 fails without correct permissions - function testERC20WithdrawFailure() public { - vm.expectRevert(bytes("CoreRef: Caller is not a PCV controller")); - psm.withdrawERC20(address(underlyingToken), address(this), 100); - } - - function testSetOracleFloorPriceNonGovernorFails() public { - vm.expectRevert("CoreRef: Caller is not a governor"); - psm.setOracleFloorPrice(100); - } - - function testSetOracleCeilingPriceNonGovernorFails() public { - vm.expectRevert("CoreRef: Caller is not a governor"); - psm.setOracleCeilingPrice(100); - } -} diff --git a/test/unit/system/System.t.sol b/test/unit/system/System.t.sol index 74a2267e0..60e9e134a 100644 --- a/test/unit/system/System.t.sol +++ b/test/unit/system/System.t.sol @@ -18,11 +18,9 @@ import {PCVDeposit} from "@voltprotocol/pcv/PCVDeposit.sol"; import {SystemEntry} from "@voltprotocol/entry/SystemEntry.sol"; import {PCVGuardian} from "@voltprotocol/pcv/PCVGuardian.sol"; import {MockCoreRefV2} from "@test/mock/MockCoreRefV2.sol"; -import {ERC20Allocator} from "@voltprotocol/pcv/ERC20Allocator.sol"; import {GenericCallMock} from "@test/mock/GenericCallMock.sol"; import {MockPCVDepositV3} from "@test/mock/MockPCVDepositV3.sol"; import {VoltSystemOracle} from "@voltprotocol/oracle/VoltSystemOracle.sol"; -import {PegStabilityModule} from "@voltprotocol/peg/PegStabilityModule.sol"; import {IPCVDepositBalances} from "@voltprotocol/pcv/IPCVDepositBalances.sol"; import {ConstantPriceOracle} from "@voltprotocol/oracle/ConstantPriceOracle.sol"; import {MorphoCompoundPCVDeposit} from "@voltprotocol/pcv/morpho/MorphoCompoundPCVDeposit.sol"; @@ -70,12 +68,9 @@ contract SystemUnitTest is Test { ICoreV2 public core; SystemEntry public entry; - PegStabilityModule public daipsm; - PegStabilityModule public usdcpsm; MockPCVDepositV3 public pcvDepositDai; MockPCVDepositV3 public pcvDepositUsdc; PCVGuardian public pcvGuardian; - ERC20Allocator public allocator; VoltSystemOracle public oracle; TimelockController public timelockController; GlobalRateLimitedMinter public grlm; @@ -104,16 +99,10 @@ contract SystemUnitTest is Test { uint256 public constant maxRateLimitPerSecondMinting = 100e18; /// replenish 500k VOLT per day - uint128 public constant rateLimitPerSecondMinting = 5.787e18; + uint64 public constant rateLimitPerSecondMinting = 5.787e18; /// buffer cap of 1.5m VOLT - uint128 public constant bufferCapMinting = 1_500_000e18; - - /// ---------- ALLOCATOR PARAMS ---------- - - uint256 public constant maxRateLimitPerSecond = 1_000e18; /// 1k volt per second - uint128 public constant rateLimitPerSecond = 10e18; /// 10 volt per second - uint128 public constant bufferCap = 1_000_000e18; /// buffer cap is 1m volt + uint96 public constant bufferCapMinting = 1_500_000e18; /// ---------- PSM PARAMS ---------- @@ -155,28 +144,6 @@ contract SystemUnitTest is Test { bufferCapMinting ); - usdcpsm = new PegStabilityModule( - coreAddress, - address(oracle), - address(0), - -12, - false, - usdc, - voltFloorPriceUsdc, - voltCeilingPriceUsdc - ); - - daipsm = new PegStabilityModule( - coreAddress, - address(oracle), - address(0), - 0, - false, - dai, - voltFloorPriceDai, - voltCeilingPriceDai - ); - pcvDepositDai = new MockPCVDepositV3(coreAddress, address(dai)); pcvDepositUsdc = new MockPCVDepositV3(coreAddress, address(usdc)); @@ -201,18 +168,15 @@ contract SystemUnitTest is Test { executorAddresses ); - address[] memory toWhitelist = new address[](4); + address[] memory toWhitelist = new address[](2); toWhitelist[0] = address(pcvDepositDai); toWhitelist[1] = address(pcvDepositUsdc); - toWhitelist[2] = address(usdcpsm); - toWhitelist[3] = address(daipsm); pcvGuardian = new PCVGuardian( coreAddress, address(timelockController), toWhitelist ); - allocator = new ERC20Allocator(coreAddress); timelockController.renounceRole( timelockController.TIMELOCK_ADMIN_ROLE(), @@ -223,7 +187,6 @@ contract SystemUnitTest is Test { core.grantPCVController(address(pcvGuardian)); core.grantPCVController(address(pcvRouter)); - core.grantPCVController(address(allocator)); core.grantPCVGuard(addresses.userAddress); core.grantPCVGuard(addresses.secondUserAddress); @@ -233,18 +196,11 @@ contract SystemUnitTest is Test { core.grantGovernor(address(timelockController)); core.grantMinter(address(grlm)); - core.grantRateLimitedMinter(address(daipsm)); - core.grantRateLimitedMinter(address(usdcpsm)); - core.grantRateLimitedRedeemer(address(daipsm)); - core.grantRateLimitedRedeemer(address(usdcpsm)); core.grantLocker(address(pcvDepositUsdc)); core.grantLocker(address(pcvDepositDai)); core.grantLocker(address(pcvGuardian)); - core.grantLocker(address(allocator)); core.grantLocker(address(pcvOracle)); - core.grantLocker(address(usdcpsm)); - core.grantLocker(address(daipsm)); core.grantLocker(address(entry)); core.grantLocker(address(grlm)); @@ -253,24 +209,8 @@ contract SystemUnitTest is Test { ); core.setGlobalReentrancyLock(lock); - allocator.connectPSM( - address(usdcpsm), - usdcTargetBalance, - usdcDecimalsNormalizer - ); - allocator.connectPSM( - address(daipsm), - daiTargetBalance, - daiDecimalsNormalizer - ); - - allocator.connectDeposit(address(usdcpsm), address(pcvDepositUsdc)); - allocator.connectDeposit(address(daipsm), address(pcvDepositDai)); - /// top up contracts with tokens for testing /// if done after the setting of pcv oracle, balances will be incorrect unless deposit is called - dai.mint(address(daipsm), daiTargetBalance); - usdc.mint(address(usdcpsm), usdcTargetBalance); dai.mint(address(pcvDepositDai), daiTargetBalance); usdc.mint(address(pcvDepositUsdc), usdcTargetBalance); @@ -295,8 +235,6 @@ contract SystemUnitTest is Test { vm.label(address(timelockController), "Timelock Controller"); vm.label(address(entry), "entry"); - vm.label(address(daipsm), "daipsm"); - vm.label(address(usdcpsm), "usdcpsm"); vm.label(address(pcvDepositDai), "pcvDepositDai"); vm.label(address(pcvDepositUsdc), "pcvDepositUsdc"); vm.label(address(this), "address this"); @@ -305,10 +243,6 @@ contract SystemUnitTest is Test { } function testSetup() public { - assertTrue(core.isLocker(address(usdcpsm))); - assertTrue(core.isLocker(address(daipsm))); - assertTrue(core.isLocker(address(allocator))); - assertTrue( !timelockController.hasRole( timelockController.TIMELOCK_ADMIN_ROLE(), @@ -375,50 +309,16 @@ contract SystemUnitTest is Test { ); assertTrue(core.isMinter(address(grlm))); - assertTrue(core.isRateLimitedMinter(address(usdcpsm))); - assertTrue(core.isRateLimitedMinter(address(daipsm))); assertEq(address(core.globalRateLimitedMinter()), address(grlm)); assertTrue(pcvGuardian.isWhitelistAddress(address(pcvDepositDai))); assertTrue(pcvGuardian.isWhitelistAddress(address(pcvDepositUsdc))); - assertTrue(pcvGuardian.isWhitelistAddress(address(usdcpsm))); - assertTrue(pcvGuardian.isWhitelistAddress(address(daipsm))); assertEq(pcvGuardian.safeAddress(), address(timelockController)); assertEq(oracle.monthlyChangeRate(), monthlyChangeRate); - { - ( - address psmToken, - uint248 psmTargetBalance, - int8 decimalsNormalizer - ) = allocator.allPSMs(address(usdcpsm)); - address psmAddress = allocator.pcvDepositToPSM( - address(pcvDepositUsdc) - ); - assertEq(psmTargetBalance, usdcTargetBalance); - assertEq(decimalsNormalizer, usdcDecimalsNormalizer); - assertEq(psmAddress, address(usdcpsm)); - assertEq(psmToken, address(usdc)); - } - { - ( - address psmToken, - uint248 psmTargetBalance, - int8 decimalsNormalizer - ) = allocator.allPSMs(address(daipsm)); - address psmAddress = allocator.pcvDepositToPSM( - address(pcvDepositDai) - ); - assertEq(psmTargetBalance, daiTargetBalance); - assertEq(decimalsNormalizer, daiDecimalsNormalizer); - assertEq(psmAddress, address(daipsm)); - assertEq(psmToken, address(dai)); - } - assertTrue(core.isPCVController(address(pcvGuardian))); - assertTrue(core.isPCVController(address(allocator))); assertTrue(core.isGovernor(address(timelockController))); assertTrue(core.isGovernor(address(core))); @@ -437,8 +337,6 @@ contract SystemUnitTest is Test { entry.deposit(address(pcvDepositUsdc)); vm.startPrank(addresses.userAddress); - pcvGuardian.withdrawAllToSafeAddress(address(daipsm)); - pcvGuardian.withdrawAllToSafeAddress(address(usdcpsm)); pcvGuardian.withdrawAllToSafeAddress(address(pcvDepositDai)); pcvGuardian.withdrawAllToSafeAddress(address(pcvDepositUsdc)); @@ -454,106 +352,7 @@ contract SystemUnitTest is Test { ); assertEq(dai.balanceOf(address(pcvDepositDai)), 0); - assertEq(dai.balanceOf(address(daipsm)), 0); - assertEq(usdc.balanceOf(address(pcvDepositUsdc)), 0); - assertEq(usdc.balanceOf(address(usdcpsm)), 0); - } - - function testMintRedeemSamePriceLosesMoneyDai(uint128 mintAmount) public { - vm.assume(mintAmount != 0); - - uint256 voltAmountOut = daipsm.getMintAmountOut(mintAmount); - - vm.assume(voltAmountOut <= grlm.buffer()); - - uint256 startingBuffer = grlm.buffer(); - assertEq(volt.balanceOf(address(this)), 0); - dai.mint(address(this), mintAmount); - uint256 startingBalance = dai.balanceOf(address(this)); - - dai.approve(address(daipsm), mintAmount); - daipsm.mint(address(this), mintAmount, voltAmountOut); - - uint256 bufferAfterMint = grlm.buffer(); - - assertEq(startingBuffer - voltAmountOut, bufferAfterMint); - - uint256 voltBalance = volt.balanceOf(address(this)); - uint256 underlyingAmountOut = daipsm.getRedeemAmountOut(voltBalance); - uint256 userStartingUnderlyingBalance = dai.balanceOf(address(this)); - - volt.approve(address(daipsm), voltBalance); - daipsm.redeem(address(this), voltBalance, underlyingAmountOut); - - uint256 userEndingUnderlyingBalance = dai.balanceOf(address(this)); - uint256 bufferAfterRedeem = grlm.buffer(); - - uint256 endingBalance = dai.balanceOf(address(this)); - - assertEq(bufferAfterRedeem - voltBalance, bufferAfterMint); /// assert buffer - assertEq( - userEndingUnderlyingBalance - underlyingAmountOut, - userStartingUnderlyingBalance - ); - assertTrue(startingBalance >= endingBalance); - assertEq(volt.balanceOf(address(daipsm)), 0); - assertEq(startingBuffer, bufferAfterRedeem); - } - - function testMintRedeemSamePriceLosesOrBreaksEvenDaiNonFuzz() public { - uint128 mintAmount = 1_000e18; - - uint256 voltAmountOut = daipsm.getMintAmountOut(mintAmount); - volt.mint(address(daipsm), voltAmountOut); - - assertEq(volt.balanceOf(address(this)), 0); - dai.mint(address(this), mintAmount); - uint256 startingBalance = dai.balanceOf(address(this)); - - dai.approve(address(daipsm), mintAmount); - daipsm.mint(address(this), mintAmount, voltAmountOut); - - uint256 voltBalance = volt.balanceOf(address(this)); - volt.approve(address(daipsm), voltBalance); - daipsm.redeem(address(this), voltBalance, 0); - - uint256 endingBalance = dai.balanceOf(address(this)); - - assertTrue(startingBalance >= endingBalance); - - assertEq(volt.balanceOf(address(daipsm)), voltAmountOut); - } - - function testMintRedeemSamePriceLosesOrBreaksEvenUsdc( - uint80 mintAmount - ) public { - vm.assume(mintAmount != 0); - - uint256 voltAmountOut = usdcpsm.getMintAmountOut(mintAmount); - vm.assume(voltAmountOut <= grlm.buffer()); /// avoid rate limit hit error - - assertEq(volt.balanceOf(address(this)), 0); - usdc.mint(address(this), mintAmount); - uint256 startingBalance = usdc.balanceOf(address(this)); - uint256 startingBuffer = grlm.buffer(); - - usdc.approve(address(usdcpsm), mintAmount); - usdcpsm.mint(address(this), mintAmount, voltAmountOut); - - uint256 bufferAfterMint = grlm.buffer(); - assertEq(bufferAfterMint + voltAmountOut, startingBuffer); - - uint256 voltBalance = volt.balanceOf(address(this)); - volt.approve(address(usdcpsm), voltBalance); - usdcpsm.redeem(address(this), voltBalance, 0); - - uint256 bufferAfterRedeem = grlm.buffer(); - uint256 endingBalance = usdc.balanceOf(address(this)); - - assertEq(bufferAfterRedeem, startingBuffer); - assertTrue(startingBalance >= endingBalance); - assertEq(volt.balanceOf(address(usdcpsm)), 0); } function _emergencyPause() private { @@ -565,32 +364,6 @@ contract SystemUnitTest is Test { assertTrue(!lock.isUnlocked()); } - function testPsmFailureOnSystemEmergencyPause() public { - _emergencyPause(); - - vm.expectRevert("GlobalReentrancyLock: invalid lock level"); - usdcpsm.mint(address(this), 0, 0); - - vm.expectRevert("GlobalReentrancyLock: invalid lock level"); - usdcpsm.redeem(address(this), 0, 0); - - vm.expectRevert("GlobalReentrancyLock: invalid lock level"); - daipsm.mint(address(this), 0, 0); - - vm.expectRevert("GlobalReentrancyLock: invalid lock level"); - daipsm.redeem(address(this), 0, 0); - } - - function testAllocatorFailureOnSystemEmergencyPause() public { - _emergencyPause(); - - vm.expectRevert("GlobalReentrancyLock: invalid lock level"); - allocator.drip(address(pcvDepositDai)); - - vm.expectRevert("GlobalReentrancyLock: invalid lock level"); - allocator.skim(address(pcvDepositDai)); - } - function testPcvGuardianFailureOnSystemEmergencyPause() public { _emergencyPause(); @@ -709,9 +482,6 @@ contract SystemUnitTest is Test { bytes4(keccak256("updateP2PIndexes(address)")) ); - vm.prank(addresses.governorAddress); - allocator.connectDeposit(address(usdcpsm), address(deposit)); - vm.expectRevert("GlobalReentrancyLock: invalid lock level"); entry.accrue(address(deposit)); diff --git a/test/unit/utils/RateLimitedV2.t.sol b/test/unit/utils/RateLimitedV2.t.sol index 4bbf834ac..b2679b040 100644 --- a/test/unit/utils/RateLimitedV2.t.sol +++ b/test/unit/utils/RateLimitedV2.t.sol @@ -4,12 +4,13 @@ import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {console} from "@forge-std/console.sol"; + import {Vm} from "@forge-std/Vm.sol"; import {Test} from "@forge-std/Test.sol"; import {ICoreV2} from "@voltprotocol/core/ICoreV2.sol"; import {PCVDeposit} from "@voltprotocol/pcv/PCVDeposit.sol"; import {VoltRoles} from "@voltprotocol/core/VoltRoles.sol"; -import {ERC20Allocator} from "@voltprotocol/pcv/ERC20Allocator.sol"; import {MockRateLimitedV2} from "@test/mock/MockRateLimitedV2.sol"; import {ERC20HoldingPCVDeposit} from "@test/mock/ERC20HoldingPCVDeposit.sol"; import {TestAddresses as addresses} from "@test/unit/utils/TestAddresses.sol"; @@ -40,13 +41,13 @@ contract UnitTestRateLimitedV2 is Test { ICoreV2 private core; /// @notice maximum rate limit per second in RateLimitedV2 - uint256 private constant maxRateLimitPerSecond = 1_000_000e18; + uint256 private constant maxRateLimitPerSecond = 10e18; /// @notice rate limit per second in RateLimitedV2 - uint128 private constant rateLimitPerSecond = 10_000e18; + uint64 private constant rateLimitPerSecond = 10e18; /// @notice buffer cap in RateLimitedV2 - uint128 private constant bufferCap = 10_000_000e18; + uint96 private constant bufferCap = 10_000_000e18; function setUp() public { core = getCoreV2(); @@ -62,7 +63,9 @@ contract UnitTestRateLimitedV2 is Test { assertEq(rlm.bufferCap(), bufferCap); assertEq(rlm.rateLimitPerSecond(), rateLimitPerSecond); assertEq(rlm.MAX_RATE_LIMIT_PER_SECOND(), maxRateLimitPerSecond); - assertEq(rlm.buffer(), bufferCap); /// buffer has not been depleted + assertEq(rlm.buffer(), bufferCap / 2); /// buffer starts at midpoint + assertEq(rlm.midPoint(), bufferCap / 2); /// mid point is buffercap / 2 + assertEq(rlm.buffer(), rlm.midPoint()); /// buffer starts at midpoint } /// ACL Tests @@ -73,15 +76,16 @@ contract UnitTestRateLimitedV2 is Test { } function testSetBufferCapGovSucceeds() public { - uint256 newBufferCap = 100_000e18; + uint96 newBufferCap = 100_000e18; vm.prank(addresses.governorAddress); vm.expectEmit(true, false, false, true, address(rlm)); emit BufferCapUpdate(bufferCap, newBufferCap); - rlm.setBufferCap(newBufferCap.toUint128()); + rlm.setBufferCap(newBufferCap); assertEq(rlm.bufferCap(), newBufferCap); - assertEq(rlm.buffer(), newBufferCap); /// buffer has not been depleted + assertEq(rlm.buffer(), newBufferCap / 2); /// buffer starts at midpoint + assertEq(rlm.buffer(), rlm.midPoint()); } function testSetRateLimitPerSecondNonGovFails() public { @@ -92,23 +96,33 @@ contract UnitTestRateLimitedV2 is Test { function testSetRateLimitPerSecondAboveMaxFails() public { vm.expectRevert("RateLimited: rateLimitPerSecond too high"); vm.prank(addresses.governorAddress); - rlm.setRateLimitPerSecond(maxRateLimitPerSecond.toUint128() + 1); + rlm.setRateLimitPerSecond(uint64(maxRateLimitPerSecond + 1)); } function testSetRateLimitPerSecondSucceeds() public { vm.prank(addresses.governorAddress); - rlm.setRateLimitPerSecond(maxRateLimitPerSecond.toUint128()); + rlm.setRateLimitPerSecond(uint64(maxRateLimitPerSecond)); assertEq(rlm.rateLimitPerSecond(), maxRateLimitPerSecond); } function testDepleteBufferFailsWhenZeroBuffer() public { - rlm.depleteBuffer(bufferCap); + rlm.depleteBuffer(rlm.midPoint()); /// fully exhaust buffer vm.expectRevert("RateLimited: no rate limit buffer"); rlm.depleteBuffer(bufferCap); } + function testReplenishBufferFailsWhenAtBufferCap() public { + console.log("\nstarting test"); + rlm.buffer(); /// where are we at? + rlm.replenishBuffer(rlm.midPoint()); /// completely fill buffer + console.log("\n"); + rlm.buffer(); /// where are we at? + vm.expectRevert("RateLimited: buffer cap overflow"); + rlm.replenishBuffer(1); + } + function testSetRateLimitPerSecondGovSucceeds() public { - uint256 newRateLimitPerSecond = 15_000e18; + uint256 newRateLimitPerSecond = rateLimitPerSecond - 1; vm.prank(addresses.governorAddress); vm.expectEmit(true, false, false, true, address(rlm)); @@ -116,29 +130,30 @@ contract UnitTestRateLimitedV2 is Test { rateLimitPerSecond, newRateLimitPerSecond ); - rlm.setRateLimitPerSecond(newRateLimitPerSecond.toUint128()); + rlm.setRateLimitPerSecond(uint64(newRateLimitPerSecond)); assertEq(rlm.rateLimitPerSecond(), newRateLimitPerSecond); } function testDepleteBuffer(uint128 amountToPull, uint16 warpAmount) public { - if (amountToPull > bufferCap) { + if (amountToPull > bufferCap / 2) { vm.expectRevert("RateLimited: rate limit hit"); rlm.depleteBuffer(amountToPull); } else { vm.expectEmit(true, false, false, true, address(rlm)); - emit BufferUsed(amountToPull, bufferCap - amountToPull); + emit BufferUsed(amountToPull, bufferCap / 2 - amountToPull); rlm.depleteBuffer(amountToPull); + uint256 endingBuffer = rlm.buffer(); - assertEq(endingBuffer, bufferCap - amountToPull); + assertEq(endingBuffer, bufferCap / 2 - amountToPull); assertEq(block.timestamp, rlm.lastBufferUsedTime()); vm.warp(block.timestamp + warpAmount); uint256 accruedBuffer = warpAmount * rateLimitPerSecond; - uint256 expectedBuffer = Math.min( + uint256 expectedBuffer = Math.min( /// only accumulate to mid point after depletion endingBuffer + accruedBuffer, - bufferCap + bufferCap / 2 ); assertEq(expectedBuffer, rlm.buffer()); } @@ -148,12 +163,12 @@ contract UnitTestRateLimitedV2 is Test { uint128 amountToReplenish, uint16 warpAmount ) public { - rlm.depleteBuffer(bufferCap); /// fully exhaust buffer + rlm.depleteBuffer(bufferCap / 2); /// fully exhaust buffer assertEq(rlm.buffer(), 0); uint256 actualAmountToReplenish = Math.min( amountToReplenish, - bufferCap + bufferCap / 2 ); vm.expectEmit(true, false, false, true, address(rlm)); emit BufferReplenished(amountToReplenish, actualAmountToReplenish); @@ -167,7 +182,7 @@ contract UnitTestRateLimitedV2 is Test { uint256 accruedBuffer = warpAmount * rateLimitPerSecond; uint256 expectedBuffer = Math.min( amountToReplenish + accruedBuffer, - bufferCap + bufferCap / 2 ); assertEq(expectedBuffer, rlm.buffer()); } From 7239980c1d55dc30b4da11156ccbc95f3dae52c2 Mon Sep 17 00:00:00 2001 From: Elliot Date: Fri, 17 Feb 2023 23:17:45 -0800 Subject: [PATCH 55/74] lint --- src/core/IPermissionsV2.sol | 6 +- src/core/PermissionsV2.sol | 124 +++++++------------- src/core/VoltRoles.sol | 3 +- src/peg/INonCustodialPSM.sol | 2 +- src/peg/NonCustodialPSM.sol | 9 +- src/rate-limits/GlobalRateLimitedMinter.sol | 1 - src/utils/RateLimitedV2.sol | 5 +- 7 files changed, 58 insertions(+), 92 deletions(-) diff --git a/src/core/IPermissionsV2.sol b/src/core/IPermissionsV2.sol index 71c566552..83ec0109d 100644 --- a/src/core/IPermissionsV2.sol +++ b/src/core/IPermissionsV2.sol @@ -6,7 +6,6 @@ import {IAccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; /// @title Permissions interface /// @author Volt Protocol interface IPermissionsV2 is IAccessControl { - // ----------- Governor only state changing api ----------- function createRole(bytes32 role, bytes32 adminRole) external; @@ -84,8 +83,5 @@ interface IPermissionsV2 is IAccessControl { /// @notice granted to peg stability modules that will call in to deplete buffer /// and mint Volt - function PSM_MINTER() - external - view - returns (bytes32); + function PSM_MINTER() external view returns (bytes32); } diff --git a/src/core/PermissionsV2.sol b/src/core/PermissionsV2.sol index fd496f541..d3e362332 100644 --- a/src/core/PermissionsV2.sol +++ b/src/core/PermissionsV2.sol @@ -69,11 +69,10 @@ contract PermissionsV2 is IPermissionsV2, AccessControlEnumerable { /// @param role the new role id /// @param adminRole the admin role id for `role` /// @dev can also be used to update admin of existing role - function createRole(bytes32 role, bytes32 adminRole) - external - override - onlyGovernor - { + function createRole( + bytes32 role, + bytes32 adminRole + ) external override onlyGovernor { _setRoleAdmin(role, adminRole); } @@ -85,11 +84,9 @@ contract PermissionsV2 is IPermissionsV2, AccessControlEnumerable { /// @notice grants controller role to address /// @param pcvController new controller - function grantPCVController(address pcvController) - external - override - onlyGovernor - { + function grantPCVController( + address pcvController + ) external override onlyGovernor { _grantRole(PCV_CONTROLLER_ROLE, pcvController); } @@ -107,11 +104,9 @@ contract PermissionsV2 is IPermissionsV2, AccessControlEnumerable { /// @notice grants level one locker role to address /// @param levelOneLocker new level one locker address - function grantLocker(address levelOneLocker) - external - override - onlyGovernor - { + function grantLocker( + address levelOneLocker + ) external override onlyGovernor { _grantRole(LOCKER_ROLE, levelOneLocker); } @@ -123,11 +118,9 @@ contract PermissionsV2 is IPermissionsV2, AccessControlEnumerable { /// @notice grants ability to mint Volt through the global rate limited minter /// @param rateLimitedMinter address to add as a minter in global rate limited minter - function grantPsmMinter(address rateLimitedMinter) - external - override - onlyGovernor - { + function grantPsmMinter( + address rateLimitedMinter + ) external override onlyGovernor { _grantRole(PSM_MINTER, rateLimitedMinter); } @@ -139,11 +132,9 @@ contract PermissionsV2 is IPermissionsV2, AccessControlEnumerable { /// @notice revokes pcvController role from address /// @param pcvController ex pcvController - function revokePCVController(address pcvController) - external - override - onlyGovernor - { + function revokePCVController( + address pcvController + ) external override onlyGovernor { _revokeRole(PCV_CONTROLLER_ROLE, pcvController); } @@ -161,11 +152,9 @@ contract PermissionsV2 is IPermissionsV2, AccessControlEnumerable { /// @notice revokes global locker role from address /// @param levelOneLocker ex globalLocker - function revokeLocker(address levelOneLocker) - external - override - onlyGovernor - { + function revokeLocker( + address levelOneLocker + ) external override onlyGovernor { _revokeRole(LOCKER_ROLE, levelOneLocker); } @@ -177,22 +166,19 @@ contract PermissionsV2 is IPermissionsV2, AccessControlEnumerable { /// @notice revokes ability to mint Volt through the global rate limited minter /// @param rateLimitedMinter ex minter in global rate limited minter - function revokePsmMinter(address rateLimitedMinter) - external - override - onlyGovernor - { + function revokePsmMinter( + address rateLimitedMinter + ) external override onlyGovernor { _revokeRole(PSM_MINTER, rateLimitedMinter); } /// @notice revokes a role from address /// @param role the role to revoke /// @param account the address to revoke the role from - function revokeOverride(bytes32 role, address account) - external - override - onlyGuardian - { + function revokeOverride( + bytes32 role, + address account + ) external override onlyGuardian { require( role != GOVERNOR_ROLE, "Permissions: Guardian cannot revoke governor" @@ -206,13 +192,9 @@ contract PermissionsV2 is IPermissionsV2, AccessControlEnumerable { /// @param _address address to check /// @return true _address is a minter // only virtual for testing mock override - function isMinter(address _address) - external - view - virtual - override - returns (bool) - { + function isMinter( + address _address + ) external view virtual override returns (bool) { return hasRole(MINTER_ROLE, _address); } @@ -220,13 +202,9 @@ contract PermissionsV2 is IPermissionsV2, AccessControlEnumerable { /// @param _address address to check /// @return true _address is a controller // only virtual for testing mock override - function isPCVController(address _address) - external - view - virtual - override - returns (bool) - { + function isPCVController( + address _address + ) external view virtual override returns (bool) { return hasRole(PCV_CONTROLLER_ROLE, _address); } @@ -234,13 +212,9 @@ contract PermissionsV2 is IPermissionsV2, AccessControlEnumerable { /// @param _address address to check /// @return true _address is a governor // only virtual for testing mock override - function isGovernor(address _address) - public - view - virtual - override - returns (bool) - { + function isGovernor( + address _address + ) public view virtual override returns (bool) { return hasRole(GOVERNOR_ROLE, _address); } @@ -248,13 +222,9 @@ contract PermissionsV2 is IPermissionsV2, AccessControlEnumerable { /// @param _address address to check /// @return true _address is a guardian // only virtual for testing mock override - function isGuardian(address _address) - public - view - virtual - override - returns (bool) - { + function isGuardian( + address _address + ) public view virtual override returns (bool) { return hasRole(GUARDIAN_ROLE, _address); } @@ -268,24 +238,18 @@ contract PermissionsV2 is IPermissionsV2, AccessControlEnumerable { /// @notice checks if address has PCV Guard role /// @param _address address to check /// @return true if _address has PCV Guard role - function isPCVGuard(address _address) - external - view - override - returns (bool) - { + function isPCVGuard( + address _address + ) external view override returns (bool) { return hasRole(PCV_GUARD_ROLE, _address); } /// @notice checks if address has Volt Minter Role /// @param _address address to check /// @return true if _address has Volt Minter Role - function isPsmMinter(address _address) - external - view - override - returns (bool) - { + function isPsmMinter( + address _address + ) external view override returns (bool) { return hasRole(PSM_MINTER, _address); } } diff --git a/src/core/VoltRoles.sol b/src/core/VoltRoles.sol index 1fd76b210..f21838330 100644 --- a/src/core/VoltRoles.sol +++ b/src/core/VoltRoles.sol @@ -40,8 +40,7 @@ library VoltRoles { /// @notice can replenish and deplete buffer through the GlobalRateLimiter. /// replenishing burns Volt, depleting mints Volt - bytes32 internal constant PSM_MINTER = - keccak256("PSM_MINTER_ROLE"); + bytes32 internal constant PSM_MINTER = keccak256("PSM_MINTER_ROLE"); /*/////////////////////////////////////////////////////////////// Minor Roles diff --git a/src/peg/INonCustodialPSM.sol b/src/peg/INonCustodialPSM.sol index 16d93f834..1f3eb112c 100644 --- a/src/peg/INonCustodialPSM.sol +++ b/src/peg/INonCustodialPSM.sol @@ -61,7 +61,7 @@ interface INonCustodialPSM { /// @notice sets the ceiling price in BP function setOracleCeilingPrice(uint128 newCeiling) external; - /// @notice set the target for sending proceeds and + /// @notice set the target for sending proceeds and function setPCVDeposit(IPCVDepositV2 newTarget) external; // ----------- Getters ----------- diff --git a/src/peg/NonCustodialPSM.sol b/src/peg/NonCustodialPSM.sol index ff4e3dba8..8f0c3aada 100644 --- a/src/peg/NonCustodialPSM.sol +++ b/src/peg/NonCustodialPSM.sol @@ -62,7 +62,8 @@ contract NonCustodialPSM is INonCustodialPSM, OracleRefV2 { backupOracleAddress, decimalNormalizer, invert - ) { + ) + { underlyingToken = underlyingTokenAddress; _setFloor(floorPrice); @@ -179,7 +180,11 @@ contract NonCustodialPSM is INonCustodialPSM, OracleRefV2 { /// ------- Interactions with Untrusted Contract ------- - underlyingToken.safeTransferFrom(msg.sender, address(pcvDeposit), amountIn); /// Interaction -- untrusted contract + underlyingToken.safeTransferFrom( + msg.sender, + address(pcvDeposit), + amountIn + ); /// Interaction -- untrusted contract pcvDeposit.deposit(); /// deposit into underlying venue to register new amount of PCV emit Mint(to, amountIn, amountVoltOut); diff --git a/src/rate-limits/GlobalRateLimitedMinter.sol b/src/rate-limits/GlobalRateLimitedMinter.sol index a75485130..9db62aa3e 100644 --- a/src/rate-limits/GlobalRateLimitedMinter.sol +++ b/src/rate-limits/GlobalRateLimitedMinter.sol @@ -13,7 +13,6 @@ import {RateLimitedV2} from "@voltprotocol/utils/RateLimitedV2.sol"; /// Peg Stability Modules will be granted the RATE_LIMIT_SYSTEM_ENTRY_REPLENISH_ROLE to replenish /// this contract's on a global rate limit when burning Volt. contract GlobalRateLimitedMinter is IGlobalRateLimitedMinter, RateLimitedV2 { - /// @param _core reference to the core smart contract /// @param _maxRateLimitPerSecond maximum rate limit per second that governance can set /// @param _rateLimitPerSecond starting rate limit per second for Volt minting diff --git a/src/utils/RateLimitedV2.sol b/src/utils/RateLimitedV2.sol index 50f58b979..b2a5f87af 100644 --- a/src/utils/RateLimitedV2.sol +++ b/src/utils/RateLimitedV2.sol @@ -137,7 +137,10 @@ abstract contract RateLimitedV2 is IRateLimitedV2, CoreRefV2 { uint256 _bufferCap = bufferCap; /// gas opti, save an SLOAD - require(newBuffer + amount <= _bufferCap, "RateLimited: buffer cap overflow"); + require( + newBuffer + amount <= _bufferCap, + "RateLimited: buffer cap overflow" + ); uint32 blockTimestamp = block.timestamp.toUint32(); From 6ce2d1ace0428f21141bd3b12ca4bc6a68ca0bc1 Mon Sep 17 00:00:00 2001 From: Elliot Date: Thu, 23 Feb 2023 13:04:47 -0800 Subject: [PATCH 56/74] RateLimitedV2 and NC PSM checkpoint --- src/peg/NonCustodialPSM.sol | 5 +- src/utils/RateLimitedV2.sol | 3 +- test/mock/MockPCVDepositV3.sol | 8 ++- test/unit/peg/UnitTestNonCustodialPSM.t.sol | 79 ++++++++++----------- 4 files changed, 49 insertions(+), 46 deletions(-) diff --git a/src/peg/NonCustodialPSM.sol b/src/peg/NonCustodialPSM.sol index 8f0c3aada..e30fc358f 100644 --- a/src/peg/NonCustodialPSM.sol +++ b/src/peg/NonCustodialPSM.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.13; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import {console} from "@forge-std/console.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; @@ -66,9 +67,9 @@ contract NonCustodialPSM is INonCustodialPSM, OracleRefV2 { { underlyingToken = underlyingTokenAddress; - _setFloor(floorPrice); - _setCeiling(ceilingPrice); _setPCVDeposit(pcvDepositAddress); + _setCeiling(ceilingPrice); + _setFloor(floorPrice); } // ----------- Governor Only State Changing API ----------- diff --git a/src/utils/RateLimitedV2.sol b/src/utils/RateLimitedV2.sol index b2a5f87af..b76620bc4 100644 --- a/src/utils/RateLimitedV2.sol +++ b/src/utils/RateLimitedV2.sol @@ -30,7 +30,7 @@ abstract contract RateLimitedV2 is IRateLimitedV2, CoreRefV2 { /// @notice the rate per second for this contract uint64 public rateLimitPerSecond; - /// @notice the cap of the buffer that can be used at once + /// @notice buffer upper limit uint96 public bufferCap; /// @notice buffercap / 2 @@ -172,6 +172,7 @@ abstract contract RateLimitedV2 is IRateLimitedV2, CoreRefV2 { uint96 newMidPoint = newBufferCap / 2; midPoint = newMidPoint; /// start at midpoint bufferCap = newBufferCap; /// set buffer cap + console.log("set buffer cap: ", bufferCap); emit BufferCapUpdate(oldBufferCap, newBufferCap); } diff --git a/test/mock/MockPCVDepositV3.sol b/test/mock/MockPCVDepositV3.sol index 44344003a..71f34fcf5 100644 --- a/test/mock/MockPCVDepositV3.sol +++ b/test/mock/MockPCVDepositV3.sol @@ -103,11 +103,11 @@ contract MockPCVDepositV3 is IPCVDeposit, CoreRefV2 { } function withdrawERC20( - address token, + address _token, address to, uint256 amount ) external override { - IERC20(token).transfer(to, amount); + IERC20(_token).transfer(to, amount); } function withdrawETH( @@ -120,4 +120,8 @@ contract MockPCVDepositV3 is IPCVDeposit, CoreRefV2 { function balance() public view override returns (uint256) { return IERC20(balanceReportedIn).balanceOf(address(this)); } + + function token() public view returns (address) { + return balanceReportedIn; + } } diff --git a/test/unit/peg/UnitTestNonCustodialPSM.t.sol b/test/unit/peg/UnitTestNonCustodialPSM.t.sol index 4c66b0b51..0449a43a3 100644 --- a/test/unit/peg/UnitTestNonCustodialPSM.t.sol +++ b/test/unit/peg/UnitTestNonCustodialPSM.t.sol @@ -5,7 +5,7 @@ import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; -import {Test} from "@forge-std/Test.sol"; +import {Test, console} from "@forge-std/Test.sol"; import {CoreV2} from "@voltprotocol/core/CoreV2.sol"; import {Constants} from "@voltprotocol/Constants.sol"; import {Deviation} from "@test/unit/utils/Deviation.sol"; @@ -202,16 +202,6 @@ contract NonCustodialPSMUnitTest is Test { ); } - function testMintFails() public { - vm.expectRevert("NonCustodialPSM: cannot mint"); - psm.mint(address(0), 0, 0); - } - - function testGetMintAmountOutFails() public { - vm.expectRevert("NonCustodialPSM: cannot mint"); - psm.getMintAmountOut(0); - } - function testExitValueInversionPositive(uint96 amount) public { psm = new NonCustodialPSM( coreAddress, @@ -300,43 +290,50 @@ contract NonCustodialPSMUnitTest is Test { vm.assume(redeemAmount != 0); uint256 amountOut; + uint256 voltBalance = volt.balanceOf(address(this)); + uint256 underlyingAmountOut = psm.getRedeemAmountOut(voltBalance); + uint256 userStartingUnderlyingBalance = dai.balanceOf(address(this)); + uint256 depositStartingUnderlyingBalance = pcvDeposit.balance(); + amountOut = underlyingAmountOut; - uint256 voltBalance = volt.balanceOf(address(this)); - uint256 underlyingAmountOut = psm.getRedeemAmountOut(voltBalance); - uint256 userStartingUnderlyingBalance = dai.balanceOf( - address(this) - ); - uint256 depositStartingUnderlyingBalance = pcvDeposit.balance(); - amountOut = underlyingAmountOut; + volt.approve(address(psm), voltBalance); + assertEq( + underlyingAmountOut, + psm.redeem(address(this), voltBalance, underlyingAmountOut) + ); + console.log("successfully redeemed"); + console.log("bufferCap: ", bufferCap); + console.log("underlyingAmountOut: ", underlyingAmountOut); + console.log( + "bufferCap - underlyingAmountOut: ", + bufferCap - underlyingAmountOut + ); - volt.approve(address(psm), voltBalance); - assertEq( - underlyingAmountOut, - psm.redeem(address(this), voltBalance, underlyingAmountOut) - ); - assertEq(bufferCap - underlyingAmountOut, grlm.buffer()); + assertEq(bufferCap - underlyingAmountOut, grlm.midPoint()); - uint256 depositEndingUnderlyingBalance = pcvDeposit.balance(); - uint256 userEndingUnderlyingBalance = dai.balanceOf(address(this)); - uint256 bufferAfterRedeem = grlm.buffer(); + uint256 depositEndingUnderlyingBalance = pcvDeposit.balance(); + uint256 userEndingUnderlyingBalance = dai.balanceOf(address(this)); + uint256 bufferAfterRedeem = grlm.buffer(); - uint256 endingBalance = dai.balanceOf(address(this)); + uint256 endingBalance = dai.balanceOf(address(this)); - assertEq(endingBalance, underlyingAmountOut); + console.log("asserted midpoint"); + assertEq(endingBalance, underlyingAmountOut); + console.log("asserted amounts out"); - assertEq( - depositStartingUnderlyingBalance - - depositEndingUnderlyingBalance, - underlyingAmountOut - ); + assertEq( + depositStartingUnderlyingBalance - depositEndingUnderlyingBalance, + underlyingAmountOut + ); + console.log("asserted deposit amounts out"); - assertEq(bufferAfterRedeem, grlm.bufferCap()); - assertEq(bufferAfterRedeem, grlm.buffer()); - assertEq( - userEndingUnderlyingBalance - underlyingAmountOut, - userStartingUnderlyingBalance - ); - assertEq(volt.balanceOf(address(psm)), 0); + assertEq(bufferAfterRedeem, amountOut); + // assertEq(bufferAfterRedeem, grlm.buffer()); + assertEq( + userEndingUnderlyingBalance - underlyingAmountOut, + userStartingUnderlyingBalance + ); + assertEq(volt.balanceOf(address(psm)), 0); } function testRedeemDifferentialSucceeds(uint128 redeemAmount) public { From 0825f6240c08d65c5c6b46d77687960dd4cb60d0 Mon Sep 17 00:00:00 2001 From: Elliot Date: Sun, 26 Feb 2023 00:24:16 -0700 Subject: [PATCH 57/74] Tests passing, cleanup comments --- src/peg/NonCustodialPSM.sol | 1 - src/utils/RateLimitedV2.sol | 11 +- .../IntegrationTestRateLimiters.sol | 3 +- .../limiter/GlobalRateLimitedMinter.t.sol | 15 ++- test/unit/peg/UnitTestNonCustodialPSM.t.sol | 79 ++++--------- test/unit/system/System.t.sol | 8 +- test/unit/utils/RateLimitedV2.t.sol | 109 +++++++++++------- 7 files changed, 104 insertions(+), 122 deletions(-) diff --git a/src/peg/NonCustodialPSM.sol b/src/peg/NonCustodialPSM.sol index e30fc358f..66cd0cc3f 100644 --- a/src/peg/NonCustodialPSM.sol +++ b/src/peg/NonCustodialPSM.sol @@ -3,7 +3,6 @@ pragma solidity 0.8.13; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; -import {console} from "@forge-std/console.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; diff --git a/src/utils/RateLimitedV2.sol b/src/utils/RateLimitedV2.sol index b76620bc4..bc47865d6 100644 --- a/src/utils/RateLimitedV2.sol +++ b/src/utils/RateLimitedV2.sol @@ -3,7 +3,6 @@ pragma solidity =0.8.13; import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; -import {console} from "@forge-std/console.sol"; import {CoreRefV2} from "@voltprotocol/refs/CoreRefV2.sol"; import {IRateLimitedV2} from "@voltprotocol/utils/IRateLimitedV2.sol"; @@ -92,10 +91,6 @@ abstract contract RateLimitedV2 is IRateLimitedV2, CoreRefV2 { uint256 cachedBufferStored = bufferStored; uint256 bufferDelta = rateLimitPerSecond * elapsed; - console.log("midPoint: ", midPoint); - console.log("bufferDelta: ", bufferDelta); - console.log("bufferStored: ", cachedBufferStored); - /// converge on mid point if (cachedBufferStored < midPoint) { /// buffer is below mid point, time accumulation can bring it back up to the mid point @@ -105,7 +100,6 @@ abstract contract RateLimitedV2 is IRateLimitedV2, CoreRefV2 { return Math.max(cachedBufferStored - bufferDelta, midPoint); } - console.log("returning buffer stored"); /// if already at mid point, do nothing return cachedBufferStored; } @@ -116,8 +110,8 @@ abstract contract RateLimitedV2 is IRateLimitedV2, CoreRefV2 { function _depleteBuffer(uint256 amount) internal { uint256 newBuffer = buffer(); - require(newBuffer != 0, "RateLimited: no rate limit buffer"); - require(amount <= newBuffer, "RateLimited: rate limit hit"); + /// this line could be removed to save on gas as calculating newBufferStored will underflow if amount is gt buffer + require(amount <= newBuffer, "RateLimited: buffer cap underflow"); uint32 blockTimestamp = block.timestamp.toUint32(); uint224 newBufferStored = (newBuffer - amount).toUint224(); @@ -172,7 +166,6 @@ abstract contract RateLimitedV2 is IRateLimitedV2, CoreRefV2 { uint96 newMidPoint = newBufferCap / 2; midPoint = newMidPoint; /// start at midpoint bufferCap = newBufferCap; /// set buffer cap - console.log("set buffer cap: ", bufferCap); emit BufferCapUpdate(oldBufferCap, newBufferCap); } diff --git a/test/integration/post-proposal-checks/IntegrationTestRateLimiters.sol b/test/integration/post-proposal-checks/IntegrationTestRateLimiters.sol index 46cf0a95f..addb967db 100644 --- a/test/integration/post-proposal-checks/IntegrationTestRateLimiters.sol +++ b/test/integration/post-proposal-checks/IntegrationTestRateLimiters.sol @@ -106,7 +106,7 @@ contract IntegrationTestRateLimiters is PostProposalCheck { uint256 largeAmount = grlm.bufferCap() * 2; vm.startPrank(user); dai.approve(address(daincpsm), largeAmount); - vm.expectRevert("RateLimited: rate limit hit"); + vm.expectRevert("RateLimited: buffer cap underflow"); daincpsm.mint(user, largeAmount, 0); vm.stopPrank(); } @@ -149,7 +149,6 @@ contract IntegrationTestRateLimiters is PostProposalCheck { deal(address(dai), morphoDaiPCVDeposit, daiAmountOut * 2); systemEntry.deposit(morphoDaiPCVDeposit); - uint256 startingBuffer = grlm.buffer(); uint256 startingDaiBalance = dai.balanceOf(user); diff --git a/test/unit/limiter/GlobalRateLimitedMinter.t.sol b/test/unit/limiter/GlobalRateLimitedMinter.t.sol index 1b48cd5e7..a624500a1 100644 --- a/test/unit/limiter/GlobalRateLimitedMinter.t.sol +++ b/test/unit/limiter/GlobalRateLimitedMinter.t.sol @@ -85,12 +85,8 @@ contract GlobalRateLimitedMinterUnitTest is Test { function testSetup() public { assertTrue(core.isMinter(address(grlm))); - assertTrue( - core.isPsmMinter(guardianAddresses.pcvGuardAddress1) - ); - assertTrue( - core.isPsmMinter(guardianAddresses.pcvGuardAddress2) - ); + assertTrue(core.isPsmMinter(guardianAddresses.pcvGuardAddress1)); + assertTrue(core.isPsmMinter(guardianAddresses.pcvGuardAddress2)); assertEq(address(core.globalRateLimitedMinter()), address(grlm)); } @@ -119,6 +115,7 @@ contract GlobalRateLimitedMinterUnitTest is Test { function testMintAsMinterSucceeds(uint80 mintAmount) public { uint256 startingBuffer = grlm.buffer(); + mintAmount = uint80(Math.min(mintAmount, grlm.midPoint())); /// avoid buffer overflow minter.mint(address(this), mintAmount); uint256 endingBuffer = grlm.buffer(); @@ -134,6 +131,12 @@ contract GlobalRateLimitedMinterUnitTest is Test { vm.prank(addresses.governorAddress); core.grantPsmMinter(address(minter)); + /// bound inputs to avoid rate limit over or under flows which would cause a revert + depleteAmount = uint80(Math.min(depleteAmount, grlm.midPoint())); + replenishAmount = uint80( + Math.min(replenishAmount, grlm.buffer() - depleteAmount) + ); + minter.mint(address(this), depleteAmount); uint256 startingBuffer = grlm.buffer(); diff --git a/test/unit/peg/UnitTestNonCustodialPSM.t.sol b/test/unit/peg/UnitTestNonCustodialPSM.t.sol index 0449a43a3..8ab680a6b 100644 --- a/test/unit/peg/UnitTestNonCustodialPSM.t.sol +++ b/test/unit/peg/UnitTestNonCustodialPSM.t.sol @@ -89,12 +89,6 @@ contract NonCustodialPSMUnitTest is Test { /// buffer cap of 1.5m VOLT uint96 public constant bufferCapMinting = 1_500_000e18; - /// ---------- ALLOCATOR PARAMS ---------- - - uint256 public constant maxRateLimitPerSecond = 1_000e18; /// 1k volt per second - uint128 public constant rateLimitPerSecond = 10e18; /// 10 volt per second - uint128 public constant bufferCap = type(uint128).max; /// buffer cap is 2^128-1 - /// ---------- PSM PARAMS ---------- uint128 public constant voltFloorPrice = 1.05e18; /// 1 volt for 1.05 dai is the minimum price @@ -168,6 +162,7 @@ contract NonCustodialPSMUnitTest is Test { entry.deposit(address(pcvDeposit)); vm.label(address(psm), "psm"); + vm.label(address(volt), "volt"); vm.label(address(pcvDeposit), "pcvDeposit"); vm.label(address(this), "address this"); } @@ -286,92 +281,58 @@ contract NonCustodialPSMUnitTest is Test { ); } - function testRedeemFuzz(uint128 redeemAmount) public { + function testRedeemFuzz(uint72 redeemAmount) public { vm.assume(redeemAmount != 0); - uint256 amountOut; + volt.mint(address(this), redeemAmount); + uint256 startingVoltSupply = volt.totalSupply(); uint256 voltBalance = volt.balanceOf(address(this)); - uint256 underlyingAmountOut = psm.getRedeemAmountOut(voltBalance); + uint256 underlyingAmountOut = psm.getRedeemAmountOut(redeemAmount); uint256 userStartingUnderlyingBalance = dai.balanceOf(address(this)); uint256 depositStartingUnderlyingBalance = pcvDeposit.balance(); - amountOut = underlyingAmountOut; volt.approve(address(psm), voltBalance); assertEq( underlyingAmountOut, psm.redeem(address(this), voltBalance, underlyingAmountOut) ); - console.log("successfully redeemed"); - console.log("bufferCap: ", bufferCap); - console.log("underlyingAmountOut: ", underlyingAmountOut); - console.log( - "bufferCap - underlyingAmountOut: ", - bufferCap - underlyingAmountOut - ); - - assertEq(bufferCap - underlyingAmountOut, grlm.midPoint()); + assertEq(grlm.midPoint() + redeemAmount, grlm.buffer()); /// redemptions make buffer increase by amount of Volt burned uint256 depositEndingUnderlyingBalance = pcvDeposit.balance(); uint256 userEndingUnderlyingBalance = dai.balanceOf(address(this)); - uint256 bufferAfterRedeem = grlm.buffer(); - uint256 endingBalance = dai.balanceOf(address(this)); - console.log("asserted midpoint"); assertEq(endingBalance, underlyingAmountOut); - console.log("asserted amounts out"); assertEq( depositStartingUnderlyingBalance - depositEndingUnderlyingBalance, underlyingAmountOut ); - console.log("asserted deposit amounts out"); - assertEq(bufferAfterRedeem, amountOut); - // assertEq(bufferAfterRedeem, grlm.buffer()); assertEq( userEndingUnderlyingBalance - underlyingAmountOut, userStartingUnderlyingBalance ); - assertEq(volt.balanceOf(address(psm)), 0); + assertEq(startingVoltSupply - volt.totalSupply(), redeemAmount); /// all redeemed Volt is burned } - function testRedeemDifferentialSucceeds(uint128 redeemAmount) public { - vm.assume(redeemAmount != 0); - - uint256 voltBalance = volt.balanceOf(address(this)); - uint256 underlyingAmountOut = psm.getRedeemAmountOut(voltBalance); - uint256 userStartingUnderlyingBalance = dai.balanceOf(address(this)); - uint256 depositStartingUnderlyingBalance = pcvDeposit.balance(); + function testRedeemWithZeroVoltFails() public { + assertEq(volt.totalSupply(), 0); + assertEq(volt.balanceOf(address(this)), 0); - volt.approve(address(psm), voltBalance); - assertEq( - underlyingAmountOut, - psm.redeem(address(this), voltBalance, underlyingAmountOut) - ); + volt.approve(address(psm), 1); - uint256 depositEndingUnderlyingBalance = pcvDeposit.balance(); - uint256 userEndingUnderlyingBalance = dai.balanceOf(address(this)); - uint256 bufferAfterRedeem = grlm.buffer(); - - uint256 endingBalance = dai.balanceOf(address(this)); - - assertEq(endingBalance, underlyingAmountOut); - - assertEq( - depositStartingUnderlyingBalance - depositEndingUnderlyingBalance, - underlyingAmountOut - ); + vm.expectRevert("ERC20: burn amount exceeds balance"); + psm.redeem(address(this), 1, 0); + } - assertEq(bufferCap - underlyingAmountOut, grlm.buffer()); + function testRedeemWithoutApprovalFails() public { + assertEq(volt.totalSupply(), 0); + assertEq(volt.balanceOf(address(this)), 0); + assertEq(volt.allowance(address(this), address(psm)), 0); - assertEq(bufferAfterRedeem, grlm.bufferCap()); - assertEq(bufferAfterRedeem, grlm.buffer()); - assertEq( - userEndingUnderlyingBalance - underlyingAmountOut, - userStartingUnderlyingBalance - ); - assertEq(volt.balanceOf(address(psm)), 0); + vm.expectRevert("ERC20: insufficient allowance"); + psm.redeem(address(this), 1, 1); } function testSetOracleFloorPriceGovernorSucceedsFuzz( diff --git a/test/unit/system/System.t.sol b/test/unit/system/System.t.sol index 60e9e134a..38ecb2674 100644 --- a/test/unit/system/System.t.sol +++ b/test/unit/system/System.t.sol @@ -335,6 +335,7 @@ contract SystemUnitTest is Test { function testPCVGuardWithdrawAllToSafeAddress() public { entry.deposit(address(pcvDepositDai)); entry.deposit(address(pcvDepositUsdc)); + vm.startPrank(addresses.userAddress); pcvGuardian.withdrawAllToSafeAddress(address(pcvDepositDai)); @@ -342,13 +343,10 @@ contract SystemUnitTest is Test { vm.stopPrank(); - assertEq( - dai.balanceOf(address(timelockController)), - daiTargetBalance * 2 - ); + assertEq(dai.balanceOf(address(timelockController)), daiTargetBalance); assertEq( usdc.balanceOf(address(timelockController)), - usdcTargetBalance * 2 + usdcTargetBalance ); assertEq(dai.balanceOf(address(pcvDepositDai)), 0); diff --git a/test/unit/utils/RateLimitedV2.t.sol b/test/unit/utils/RateLimitedV2.t.sol index b2679b040..bc6d9e0d5 100644 --- a/test/unit/utils/RateLimitedV2.t.sol +++ b/test/unit/utils/RateLimitedV2.t.sol @@ -49,6 +49,9 @@ contract UnitTestRateLimitedV2 is Test { /// @notice buffer cap in RateLimitedV2 uint96 private constant bufferCap = 10_000_000e18; + /// @notice mid point in RateLimitedV2 + uint96 private constant midPoint = bufferCap / 2; + function setUp() public { core = getCoreV2(); rlm = new MockRateLimitedV2( @@ -84,8 +87,7 @@ contract UnitTestRateLimitedV2 is Test { rlm.setBufferCap(newBufferCap); assertEq(rlm.bufferCap(), newBufferCap); - assertEq(rlm.buffer(), newBufferCap / 2); /// buffer starts at midpoint - assertEq(rlm.buffer(), rlm.midPoint()); + assertEq(rlm.buffer(), bufferCap / 2); /// buffer starts at previous midpoint } function testSetRateLimitPerSecondNonGovFails() public { @@ -107,15 +109,13 @@ contract UnitTestRateLimitedV2 is Test { function testDepleteBufferFailsWhenZeroBuffer() public { rlm.depleteBuffer(rlm.midPoint()); /// fully exhaust buffer - vm.expectRevert("RateLimited: no rate limit buffer"); + vm.expectRevert("RateLimited: buffer cap underflow"); rlm.depleteBuffer(bufferCap); } function testReplenishBufferFailsWhenAtBufferCap() public { - console.log("\nstarting test"); rlm.buffer(); /// where are we at? rlm.replenishBuffer(rlm.midPoint()); /// completely fill buffer - console.log("\n"); rlm.buffer(); /// where are we at? vm.expectRevert("RateLimited: buffer cap overflow"); rlm.replenishBuffer(1); @@ -137,7 +137,7 @@ contract UnitTestRateLimitedV2 is Test { function testDepleteBuffer(uint128 amountToPull, uint16 warpAmount) public { if (amountToPull > bufferCap / 2) { - vm.expectRevert("RateLimited: rate limit hit"); + vm.expectRevert("RateLimited: buffer cap underflow"); rlm.depleteBuffer(amountToPull); } else { vm.expectEmit(true, false, false, true, address(rlm)); @@ -150,7 +150,8 @@ contract UnitTestRateLimitedV2 is Test { vm.warp(block.timestamp + warpAmount); - uint256 accruedBuffer = warpAmount * rateLimitPerSecond; + uint256 accruedBuffer = uint256(warpAmount) * + uint256(rateLimitPerSecond); uint256 expectedBuffer = Math.min( /// only accumulate to mid point after depletion endingBuffer + accruedBuffer, bufferCap / 2 @@ -163,67 +164,95 @@ contract UnitTestRateLimitedV2 is Test { uint128 amountToReplenish, uint16 warpAmount ) public { - rlm.depleteBuffer(bufferCap / 2); /// fully exhaust buffer + rlm.depleteBuffer(midPoint); /// fully exhaust buffer assertEq(rlm.buffer(), 0); - uint256 actualAmountToReplenish = Math.min( - amountToReplenish, - bufferCap / 2 - ); + uint256 actualAmountToReplenish = Math.min(amountToReplenish, midPoint); vm.expectEmit(true, false, false, true, address(rlm)); - emit BufferReplenished(amountToReplenish, actualAmountToReplenish); + emit BufferReplenished( + actualAmountToReplenish, + actualAmountToReplenish + ); - rlm.replenishBuffer(amountToReplenish); + rlm.replenishBuffer(actualAmountToReplenish); assertEq(rlm.buffer(), actualAmountToReplenish); assertEq(block.timestamp, rlm.lastBufferUsedTime()); vm.warp(block.timestamp + warpAmount); - uint256 accruedBuffer = warpAmount * rateLimitPerSecond; - uint256 expectedBuffer = Math.min( - amountToReplenish + accruedBuffer, - bufferCap / 2 - ); - assertEq(expectedBuffer, rlm.buffer()); + uint256 cachedBufferStored = rlm.bufferStored(); + uint256 bufferDelta = rateLimitPerSecond * uint256(warpAmount); + uint256 convergedAmount = _converge(cachedBufferStored, bufferDelta); + + assertEq(convergedAmount, rlm.buffer()); } function testDepleteThenReplenishBuffer( uint128 amountToDeplete, - uint128 amountToReplenish, - uint16 warpAmount + uint128 amountToReplenish ) public { - uint256 actualAmountToDeplete = Math.min(amountToDeplete, bufferCap); + uint256 actualAmountToDeplete = Math.min(amountToDeplete, midPoint); /// bound input to less than or equal to the midPoint rlm.depleteBuffer(actualAmountToDeplete); /// deplete buffer - assertEq(rlm.buffer(), bufferCap - actualAmountToDeplete); + assertEq(rlm.buffer(), midPoint - actualAmountToDeplete); + /// either fill up the buffer, or partially refill uint256 actualAmountToReplenish = Math.min( amountToReplenish, - bufferCap + bufferCap - rlm.buffer() ); - rlm.replenishBuffer(amountToReplenish); - uint256 finalState = bufferCap - + rlm.replenishBuffer(actualAmountToReplenish); + uint256 finalState = midPoint - actualAmountToDeplete + actualAmountToReplenish; - uint256 endingBuffer = Math.min(finalState, bufferCap); - assertEq(rlm.buffer(), endingBuffer); + assertEq(rlm.buffer(), finalState); assertEq(block.timestamp, rlm.lastBufferUsedTime()); - vm.warp(block.timestamp + warpAmount); + vm.warp(block.timestamp + 500_000); /// 500k seconds * 10 Volt per second means the buffer should be back at the midpoint even if 0 replenishing occurred - uint256 accruedBuffer = warpAmount * rateLimitPerSecond; - uint256 expectedBuffer = Math.min( - finalState + accruedBuffer, - bufferCap - ); - assertEq(expectedBuffer, rlm.buffer()); + assertEq(rlm.buffer(), midPoint); } - function testReplenishWhenAtBufferCapHasNoEffect( - uint128 amountToReplenish + function testReplenishThenDepleteBuffer( + uint128 amountToReplenish, + uint128 amountToDeplete ) public { - rlm.replenishBuffer(amountToReplenish); - assertEq(rlm.buffer(), bufferCap); + uint256 actualAmountToReplenish = Math.min(amountToReplenish, midPoint); + rlm.replenishBuffer(actualAmountToReplenish); + + uint256 actualAmountToDeplete = Math.min(rlm.buffer(), amountToDeplete); /// bound input to less than or equal to the current buffer, another way to say this is bufferCap - actualAmountToReplenish + + rlm.depleteBuffer(actualAmountToDeplete); /// deplete buffer + + assertEq(rlm.buffer(), midPoint - actualAmountToDeplete); + + /// either fill up the buffer, or partially refill + + uint256 finalState = midPoint + + actualAmountToReplenish - + actualAmountToDeplete; + + assertEq(rlm.buffer(), finalState); assertEq(block.timestamp, rlm.lastBufferUsedTime()); + + vm.warp(block.timestamp + 500_000); /// 500k seconds * 10 Volt per second means the buffer should be back at the midpoint even if 0 replenishing occurred + + assertEq(rlm.buffer(), midPoint); + } + + function _converge( + uint256 cachedBufferStored, + uint256 bufferDelta + ) private pure returns (uint256) { + /// converge on mid point + if (cachedBufferStored < midPoint) { + /// buffer is below mid point, time accumulation can bring it back up to the mid point + return Math.min(cachedBufferStored + bufferDelta, midPoint); + } else if (cachedBufferStored > midPoint) { + /// buffer is above the mid point, time accumulation can bring it back down to the mid point + return Math.max(cachedBufferStored - bufferDelta, midPoint); + } + + return midPoint; } } From 311baffd45eac003c66abff126e9ef7997ec81d9 Mon Sep 17 00:00:00 2001 From: Elliot Date: Mon, 27 Feb 2023 15:22:08 -0700 Subject: [PATCH 58/74] fix unit test --- test/unit/utils/RateLimitedV2.t.sol | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/unit/utils/RateLimitedV2.t.sol b/test/unit/utils/RateLimitedV2.t.sol index bc6d9e0d5..03f689ed6 100644 --- a/test/unit/utils/RateLimitedV2.t.sol +++ b/test/unit/utils/RateLimitedV2.t.sol @@ -224,10 +224,7 @@ contract UnitTestRateLimitedV2 is Test { rlm.depleteBuffer(actualAmountToDeplete); /// deplete buffer - assertEq(rlm.buffer(), midPoint - actualAmountToDeplete); - /// either fill up the buffer, or partially refill - uint256 finalState = midPoint + actualAmountToReplenish - actualAmountToDeplete; From 0f5cf38e4aaa3c8c36d31cd1ab370393f64023e9 Mon Sep 17 00:00:00 2001 From: Elliot Date: Mon, 27 Feb 2023 17:08:59 -0700 Subject: [PATCH 59/74] Fix imports, remove comments --- src/rate-limits/GlobalRateLimitedMinter.sol | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/rate-limits/GlobalRateLimitedMinter.sol b/src/rate-limits/GlobalRateLimitedMinter.sol index 9db62aa3e..7e45f9b22 100644 --- a/src/rate-limits/GlobalRateLimitedMinter.sol +++ b/src/rate-limits/GlobalRateLimitedMinter.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity =0.8.13; -import {IGlobalRateLimitedMinter} from "@voltprotocol/rate-limits/IGlobalRateLimitedMinter.sol"; import {CoreRefV2} from "@voltprotocol/refs/CoreRefV2.sol"; import {VoltRoles} from "@voltprotocol/core/VoltRoles.sol"; import {RateLimitedV2} from "@voltprotocol/utils/RateLimitedV2.sol"; +import {IGlobalRateLimitedMinter} from "@voltprotocol/rate-limits/IGlobalRateLimitedMinter.sol"; /// @notice contract to mint Volt on a rate limit. /// All minting should flow through this smart contract. @@ -61,7 +61,3 @@ contract GlobalRateLimitedMinter is IGlobalRateLimitedMinter, RateLimitedV2 { _replenishBuffer(amount); /// effects } } - -/// two goals: -/// 1. cap the Volt supply and creation of new Volt to 5.8m -/// 2. slow the redemption of Volt to only allow .5m per day to leave the system. From 3b85ae2a79dc1880b46b531f2152d13fba860512 Mon Sep 17 00:00:00 2001 From: Elliot Date: Mon, 27 Feb 2023 17:09:45 -0700 Subject: [PATCH 60/74] remove all v1 migrator contracts --- src/v1-migration/IMigratorRouter.sol | 20 ------ src/v1-migration/IVoltMigrator.sol | 34 ---------- src/v1-migration/MigratorRouter.sol | 78 ----------------------- src/v1-migration/VoltMigrator.sol | 94 ---------------------------- 4 files changed, 226 deletions(-) delete mode 100644 src/v1-migration/IMigratorRouter.sol delete mode 100644 src/v1-migration/IVoltMigrator.sol delete mode 100644 src/v1-migration/MigratorRouter.sol delete mode 100644 src/v1-migration/VoltMigrator.sol diff --git a/src/v1-migration/IMigratorRouter.sol b/src/v1-migration/IMigratorRouter.sol deleted file mode 100644 index 817f3bcd2..000000000 --- a/src/v1-migration/IMigratorRouter.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity =0.8.13; - -interface IMigratorRouter { - /// @notice This lets the user redeem DAI using old VOLT - /// @param amountVoltIn the amount of old VOLT being deposited - /// @param minAmountOut the minimum amount of DAI the user expects to receive - function redeemDai( - uint256 amountVoltIn, - uint256 minAmountOut - ) external returns (uint256); - - /// @notice This lets the user redeem USDC using old VOLT - /// @param amountVoltIn the amount of old VOLT being deposited - /// @param minAmountOut the minimum amount of USDC the user expects to receive - function redeemUSDC( - uint256 amountVoltIn, - uint256 minAmountOut - ) external returns (uint256); -} diff --git a/src/v1-migration/IVoltMigrator.sol b/src/v1-migration/IVoltMigrator.sol deleted file mode 100644 index 6ff31936b..000000000 --- a/src/v1-migration/IVoltMigrator.sol +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity =0.8.13; - -interface IVoltMigrator { - // ----------- Events ----------- - - event VoltMigrated( - address indexed from, - address indexed to, - uint256 indexed amount - ); - - // ----------- State changing API ----------- - - /// @notice function to exchange old VOLT for new VOLT - /// @param amount the amount of old VOLT user wishes to exchange - function exchange(uint256 amount) external; - - /// @notice function to exchange old VOLT for new VOLT - /// takes the minimum of users old VOLT balance, or the amount - /// user has approved to the migrator contract & exchanges for new VOLT - function exchangeAll() external; - - /// @notice function to exchange old VOLT for new VOLT - /// @param amount the amount of old VOLT user wishes to exchange - /// @param to address to send the new VOLT to - function exchangeTo(address to, uint256 amount) external; - - /// @notice function to exchange old VOLT for new VOLT - /// takes the minimum of users old VOLT balance, or the amount - /// user has approved to the migrator contract & exchanges for new VOLT - /// @param to address to send the new VOLT to - function exchangeAllTo(address to) external; -} diff --git a/src/v1-migration/MigratorRouter.sol b/src/v1-migration/MigratorRouter.sol deleted file mode 100644 index 13b2f5bbf..000000000 --- a/src/v1-migration/MigratorRouter.sol +++ /dev/null @@ -1,78 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity =0.8.13; - -import {IMigratorRouter} from "@voltprotocol/v1-migration/IMigratorRouter.sol"; -import {IPegStabilityModule} from "@voltprotocol/peg/IPegStabilityModule.sol"; -import {IVolt} from "@voltprotocol/volt/IVolt.sol"; -import {IVoltMigrator} from "@voltprotocol/v1-migration/IVoltMigrator.sol"; - -/// @title Migrator Router -/// @notice This contract is a router that wraps around the token migrator from -/// the old volt ERC20 token to the new ERC20 token, to allow users to redeem for -/// stables using the old volt version once the new version is live -contract MigratorRouter is IMigratorRouter { - /// @notice the old VOLT token - IVolt public constant OLD_VOLT = - IVolt(0x559eBC30b0E58a45Cc9fF573f77EF1e5eb1b3E18); - - /// @notice VOLT-DAI PSM to swap between the two assets - IPegStabilityModule public immutable daiPSM; - - /// @notice VOLT-USDC PSM to swap between the two assets - IPegStabilityModule public immutable usdcPSM; - - /// @notice address of the new VOLT token - IVolt public immutable newVolt; - - /// @notice the VOLT migrator contract to swap from old VOLT to new - IVoltMigrator public immutable voltMigrator; - - constructor( - IVolt _newVolt, - IVoltMigrator _voltMigrator, - IPegStabilityModule _daiPSM, - IPegStabilityModule _usdcPSM - ) { - newVolt = _newVolt; - voltMigrator = _voltMigrator; - - daiPSM = _daiPSM; - usdcPSM = _usdcPSM; - - /// It's safe to give the following contracts max approval as they - /// are part of the Volt system and therefore we can be very confident - /// in their behavior, as such the scope of attack of giving the following - /// contracts max approvals is very limited. Also the only time the migrator - /// contract uses the transferFrom it passes msg.sender, so the only way to spend - /// the approval is via the person who gave the approval requesting the transfer - OLD_VOLT.approve(address(voltMigrator), type(uint256).max); - newVolt.approve(address(daiPSM), type(uint256).max); - newVolt.approve(address(usdcPSM), type(uint256).max); - } - - /// @notice This lets the user redeem DAI using old VOLT - /// @param amountVoltIn the amount of old VOLT being deposited - /// @param minAmountOut the minimum amount of DAI the user expects to receive - function redeemDai( - uint256 amountVoltIn, - uint256 minAmountOut - ) external returns (uint256 amountOut) { - OLD_VOLT.transferFrom(msg.sender, address(this), amountVoltIn); - voltMigrator.exchange(amountVoltIn); - - amountOut = daiPSM.redeem(msg.sender, amountVoltIn, minAmountOut); - } - - /// @notice This lets the user redeem USDC using old VOLT - /// @param amountVoltIn the amount of old VOLT being deposited - /// @param minAmountOut the minimum amount of USDC the user expects to receive - function redeemUSDC( - uint256 amountVoltIn, - uint256 minAmountOut - ) external returns (uint256 amountOut) { - OLD_VOLT.transferFrom(msg.sender, address(this), amountVoltIn); - voltMigrator.exchange(amountVoltIn); - - amountOut = usdcPSM.redeem(msg.sender, amountVoltIn, minAmountOut); - } -} diff --git a/src/v1-migration/VoltMigrator.sol b/src/v1-migration/VoltMigrator.sol deleted file mode 100644 index a77e3fee8..000000000 --- a/src/v1-migration/VoltMigrator.sol +++ /dev/null @@ -1,94 +0,0 @@ -//SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity =0.8.13; -import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; - -import {CoreRefV2} from "@voltprotocol/refs/CoreRefV2.sol"; -import {Volt} from "@voltprotocol/v1/Volt.sol"; -import {IVolt} from "@voltprotocol/volt/IVolt.sol"; -import {IVoltMigrator} from "@voltprotocol/v1-migration/IVoltMigrator.sol"; - -/// @title Volt Migrator -/// @notice This contract is used to allow user to migrate from the old VOLT token -/// to the new VOLT token that will be able to participate in the Volt Veto Module. -/// users will deposit their old VOLT token which'll be burnt, and the new Volt token -/// will be minted to them. -contract VoltMigrator is IVoltMigrator, CoreRefV2 { - using SafeERC20 for IERC20; - - /// @notice address of the old VOLT token - Volt public constant OLD_VOLT = - Volt(0x559eBC30b0E58a45Cc9fF573f77EF1e5eb1b3E18); - - /// @notice address of the new VOLT token - IVolt public immutable newVolt; - - constructor(address core, IVolt _newVolt) CoreRefV2(core) { - newVolt = _newVolt; - } - - /// @notice function to exchange old VOLT for new VOLT - /// @param amount the amount of old VOLT user wishes to exchange - function exchange(uint256 amount) external { - _migrateVolt(msg.sender, amount); - } - - /// @notice function to exchange old VOLT for new VOLT - /// takes the minimum of users old VOLT balance, or the amount - /// user has approved to the migrator contract & exchanges for new VOLT - function exchangeAll() external { - uint256 amountToExchange = _calculateAmountToExchange(); - _migrateVolt(msg.sender, amountToExchange); - } - - /// @notice function to exchange old VOLT for new VOLT - /// @param amount the amount of old VOLT user wishes to exchange - /// @param to address to send the new VOLT to - function exchangeTo(address to, uint256 amount) external { - _migrateVolt(to, amount); - } - - /// @notice function to exchange old VOLT for new VOLT - /// takes the minimum of users old VOLT balance, or the amount - /// user has approved to the migrator contract & exchanges for new VOLT - /// @param to address to send the new VOLT to - function exchangeAllTo(address to) external { - uint256 amountToExchange = _calculateAmountToExchange(); - _migrateVolt(to, amountToExchange); - } - - function _migrateVolt(address to, uint256 amount) internal { - OLD_VOLT.burnFrom(msg.sender, amount); - newVolt.transfer(to, amount); - - emit VoltMigrated(msg.sender, to, amount); - } - - function _calculateAmountToExchange() internal view returns (uint256) { - uint256 amountToExchange = Math.min( - OLD_VOLT.balanceOf(msg.sender), - OLD_VOLT.allowance(msg.sender, address(this)) - ); - require(amountToExchange != 0, "VoltMigrator: no amount to exchange"); - return amountToExchange; - } - - /// @notice sweep target token, - /// @param token to sweep - /// @param to recipient - /// @param amount of token to be sent - function sweep( - address token, - address to, - uint256 amount - ) external override onlyGovernor { - /// this check is worthless because emergency action will allow - /// arbitrary calldata with arbitrary addresses - require( - token != address(newVolt), - "VoltMigrator: cannot sweep new Volt" - ); - IERC20(token).safeTransfer(to, amount); - } -} From 9ad1c0049c18a7db3b376c9b2d6131693b1a7dcf Mon Sep 17 00:00:00 2001 From: Elliot Date: Mon, 27 Feb 2023 17:11:32 -0700 Subject: [PATCH 61/74] fix integration tests and vip16, remove deployment of deprecated contracts --- .../IntegrationTestPCVGuardian.sol | 4 +- .../IntegrationTestRateLimiters.sol | 5 +- .../IntegrationTestVoltV1Migration.sol | 509 ------------------ .../IntegrationTestProposalPSMOracle.sol | 2 +- test/proposals/vips/vip16.sol | 58 +- 5 files changed, 12 insertions(+), 566 deletions(-) delete mode 100644 test/integration/post-proposal-checks/IntegrationTestVoltV1Migration.sol diff --git a/test/integration/post-proposal-checks/IntegrationTestPCVGuardian.sol b/test/integration/post-proposal-checks/IntegrationTestPCVGuardian.sol index 8a506a91c..dae726226 100644 --- a/test/integration/post-proposal-checks/IntegrationTestPCVGuardian.sol +++ b/test/integration/post-proposal-checks/IntegrationTestPCVGuardian.sol @@ -9,9 +9,7 @@ import {IPCVDepositV2} from "@voltprotocol/pcv/IPCVDepositV2.sol"; contract IntegrationTestPCVGuardian is PostProposalCheck { function testWithdrawAllToSafeAddress() public { - address[8] memory addressesToClean = [ - addresses.mainnet("PSM_DAI"), - addresses.mainnet("PSM_USDC"), + address[6] memory addressesToClean = [ addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_DAI"), addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_USDC"), addresses.mainnet("PCV_DEPOSIT_EULER_DAI"), diff --git a/test/integration/post-proposal-checks/IntegrationTestRateLimiters.sol b/test/integration/post-proposal-checks/IntegrationTestRateLimiters.sol index addb967db..f9bf9ca1e 100644 --- a/test/integration/post-proposal-checks/IntegrationTestRateLimiters.sol +++ b/test/integration/post-proposal-checks/IntegrationTestRateLimiters.sol @@ -67,9 +67,6 @@ contract IntegrationTestRateLimiters is PostProposalCheck { // number of moved funds for tests uint256 amount = initialBuffer / 2; - // read initial pcv - uint256 startLiquidPcv = pcvOracle.getTotalPcv(); - // user performs the first mint with DAI vm.startPrank(user); dai.approve(address(daincpsm), amount); @@ -112,6 +109,8 @@ contract IntegrationTestRateLimiters is PostProposalCheck { } function testRedeemsDaiPsm(uint88 voltAmount) public { + vm.assume(voltAmount >= 1e18); + testUserPSMMint(); vm.revertTo(snapshotAfterMints); diff --git a/test/integration/post-proposal-checks/IntegrationTestVoltV1Migration.sol b/test/integration/post-proposal-checks/IntegrationTestVoltV1Migration.sol deleted file mode 100644 index 5f68808de..000000000 --- a/test/integration/post-proposal-checks/IntegrationTestVoltV1Migration.sol +++ /dev/null @@ -1,509 +0,0 @@ -// // SPDX-License-Identifier: GPL-3.0-or-later -// pragma solidity 0.8.13; - -// import {PostProposalCheck} from "@test/integration/post-proposal-checks/PostProposalCheck.sol"; - -// import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -// import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; -// import {TimelockController} from "@openzeppelin/contracts/governance/TimelockController.sol"; - -// import {IVolt} from "@voltprotocol/volt/IVolt.sol"; -// import {VoltV2} from "@voltprotocol/volt/VoltV2.sol"; -// import {CoreV2} from "@voltprotocol/core/CoreV2.sol"; -// import {stdError} from "@forge-std/StdError.sol"; -// import {MigratorRouter} from "@voltprotocol/v1-migration/MigratorRouter.sol"; -// import {VoltSystemOracle} from "@voltprotocol/oracle/VoltSystemOracle.sol"; -// import {IVoltMigrator, VoltMigrator} from "@voltprotocol/v1-migration/VoltMigrator.sol"; - -// contract IntegrationTestVoltV1Migration is PostProposalCheck { -// using SafeCast for *; - -// uint224 public constant mintAmount = 100_000_000e18; - -// TimelockController private timelockController; -// IVolt private oldVolt; -// VoltV2 private volt; -// VoltMigrator private voltMigrator; -// MigratorRouter private migratorRouter; -// PegStabilityModule private usdcpsm; -// PegStabilityModule private daipsm; -// IERC20 private dai; -// IERC20 private usdc; -// VoltSystemOracle private vso; -// address private grlm; -// address private multisig; -// address private coreV1; - -// function setUp() public override { -// super.setUp(); - -// timelockController = TimelockController( -// payable(addresses.mainnet("TIMELOCK_CONTROLLER")) -// ); -// oldVolt = IVolt(addresses.mainnet("V1_VOLT")); -// volt = VoltV2(addresses.mainnet("VOLT")); -// voltMigrator = VoltMigrator(addresses.mainnet("V1_MIGRATION_MIGRATOR")); -// migratorRouter = MigratorRouter( -// addresses.mainnet("V1_MIGRATION_ROUTER") -// ); -// usdcpsm = PegStabilityModule(addresses.mainnet("PSM_USDC")); -// daipsm = PegStabilityModule(addresses.mainnet("PSM_DAI")); -// dai = IERC20(addresses.mainnet("DAI")); -// usdc = IERC20(addresses.mainnet("USDC")); -// vso = VoltSystemOracle(addresses.mainnet("VOLT_SYSTEM_ORACLE")); -// grlm = addresses.mainnet("GLOBAL_RATE_LIMITED_MINTER"); -// multisig = addresses.mainnet("GOVERNOR"); -// coreV1 = addresses.mainnet("V1_CORE"); -// } - -// function testExchangeTo(uint64 amountOldVoltToExchange) public { -// oldVolt.approve(address(voltMigrator), type(uint256).max); -// deal(address(oldVolt), address(this), amountOldVoltToExchange); -// deal(address(volt), address(voltMigrator), amountOldVoltToExchange); - -// uint256 newVoltTotalSupply = volt.totalSupply(); -// uint256 oldVoltTotalSupply = oldVolt.totalSupply(); - -// uint256 oldVoltBalanceBefore = oldVolt.balanceOf(address(this)); -// uint256 newVoltBalanceBefore = volt.balanceOf(address(0xFFF)); - -// voltMigrator.exchangeTo(address(0xFFF), amountOldVoltToExchange); - -// uint256 oldVoltBalanceAfter = oldVolt.balanceOf(address(this)); -// uint256 newVoltBalanceAfter = volt.balanceOf(address(0xFFF)); - -// assertEq( -// newVoltBalanceAfter, -// newVoltBalanceBefore + amountOldVoltToExchange -// ); -// assertEq( -// oldVoltBalanceAfter, -// oldVoltBalanceBefore - amountOldVoltToExchange -// ); -// assertEq( -// oldVolt.totalSupply(), -// oldVoltTotalSupply - amountOldVoltToExchange -// ); -// assertEq(volt.totalSupply(), newVoltTotalSupply); /// new volt supply remains unchanged -// } - -// function testExchangeAllTo() public { -// uint256 amountOldVoltToExchange = 10_000e18; - -// oldVolt.approve(address(voltMigrator), type(uint256).max); - -// deal(address(oldVolt), address(this), amountOldVoltToExchange); -// deal(address(volt), address(voltMigrator), amountOldVoltToExchange); - -// uint256 newVoltTotalSupply = volt.totalSupply(); -// uint256 oldVoltTotalSupply = oldVolt.totalSupply(); - -// uint256 oldVoltBalanceBefore = oldVolt.balanceOf(address(this)); -// uint256 newVoltBalanceBefore = volt.balanceOf(address(0xFFF)); - -// voltMigrator.exchangeAllTo(address(0xFFF)); - -// uint256 oldVoltBalanceAfter = oldVolt.balanceOf(address(this)); -// uint256 newVoltBalanceAfter = volt.balanceOf(address(0xFFF)); - -// assertEq( -// newVoltBalanceAfter, -// newVoltBalanceBefore + oldVoltBalanceBefore -// ); -// assertEq(oldVoltBalanceAfter, 0); -// assertEq( -// oldVolt.totalSupply(), -// oldVoltTotalSupply - oldVoltBalanceBefore -// ); -// assertEq(volt.totalSupply(), newVoltTotalSupply); -// } - -// function testExchangeFailsWhenApprovalNotGiven() public { -// vm.expectRevert("ERC20: burn amount exceeds allowance"); -// voltMigrator.exchange(1e18); -// } - -// function testExchangeToFailsWhenApprovalNotGiven() public { -// vm.expectRevert("ERC20: burn amount exceeds allowance"); -// voltMigrator.exchangeTo(address(0xFFF), 1e18); -// } - -// function testExchangeFailsMigratorUnderfunded() public { -// uint256 amountOldVoltToExchange = 100_000_000e18; - -// vm.prank(grlm); -// volt.mint(address(voltMigrator), amountOldVoltToExchange); -// deal(address(oldVolt), address(this), amountOldVoltToExchange); - -// oldVolt.approve(address(voltMigrator), type(uint256).max); - -// vm.prank(address(voltMigrator)); -// volt.burn(mintAmount); - -// vm.expectRevert(stdError.arithmeticError); -// voltMigrator.exchange(amountOldVoltToExchange); -// } - -// function testExchangeAllFailsMigratorUnderfunded() public { -// oldVolt.approve(address(voltMigrator), type(uint256).max); -// deal(address(oldVolt), address(this), mintAmount); - -// vm.prank(grlm); -// volt.mint(address(voltMigrator), mintAmount); - -// vm.prank(address(voltMigrator)); -// volt.burn(mintAmount); - -// vm.expectRevert(stdError.arithmeticError); -// voltMigrator.exchangeAll(); -// } - -// function testExchangeToFailsMigratorUnderfunded() public { -// deal(address(oldVolt), address(this), mintAmount); - -// vm.prank(grlm); -// volt.mint(address(voltMigrator), mintAmount); - -// uint256 amountOldVoltToExchange = 100_000_000e18; -// oldVolt.approve(address(voltMigrator), type(uint256).max); - -// vm.prank(address(voltMigrator)); -// volt.burn(mintAmount); - -// vm.expectRevert(stdError.arithmeticError); -// voltMigrator.exchangeTo(address(0xFFF), amountOldVoltToExchange); -// } - -// function testExchangeAllToFailsMigratorUnderfunded() public { -// vm.prank(grlm); -// volt.mint(address(voltMigrator), mintAmount); -// deal(address(oldVolt), address(this), mintAmount); - -// oldVolt.approve(address(voltMigrator), type(uint256).max); - -// vm.prank(address(voltMigrator)); -// volt.burn(mintAmount); - -// vm.expectRevert(stdError.arithmeticError); -// voltMigrator.exchangeAllTo(address(0xFFF)); -// } - -// function testExchangeAllWhenApprovalNotGiven() public { -// uint256 oldVoltBalanceBefore = oldVolt.balanceOf(address(this)); -// uint256 newVoltBalanceBefore = volt.balanceOf(address(this)); - -// vm.expectRevert("VoltMigrator: no amount to exchange"); -// voltMigrator.exchangeAll(); - -// uint256 oldVoltBalanceAfter = oldVolt.balanceOf(address(this)); -// uint256 newVoltBalanceAfter = volt.balanceOf(address(this)); - -// assertEq(oldVoltBalanceBefore, oldVoltBalanceAfter); -// assertEq(newVoltBalanceBefore, newVoltBalanceAfter); -// } - -// function testExchangeAllToWhenApprovalNotGiven() public { -// uint256 oldVoltBalanceBefore = oldVolt.balanceOf(address(this)); -// uint256 newVoltBalanceBefore = volt.balanceOf(address(0xFFF)); - -// vm.expectRevert("VoltMigrator: no amount to exchange"); -// voltMigrator.exchangeAllTo(address(0xFFF)); - -// uint256 oldVoltBalanceAfter = oldVolt.balanceOf(address(this)); -// uint256 newVoltBalanceAfter = volt.balanceOf(address(0xFFF)); - -// assertEq(oldVoltBalanceBefore, oldVoltBalanceAfter); -// assertEq(newVoltBalanceBefore, newVoltBalanceAfter); -// } - -// function testExchangeAllPartialApproval() public { -// deal(address(oldVolt), address(this), 100_000e18); - -// uint256 amountOldVoltToExchange = oldVolt.balanceOf(address(this)) / 2; // exchange half of users balance - -// oldVolt.approve(address(voltMigrator), amountOldVoltToExchange); - -// vm.prank(grlm); -// volt.mint(address(voltMigrator), amountOldVoltToExchange); - -// uint256 newVoltTotalSupply = volt.totalSupply(); -// uint256 oldVoltTotalSupply = oldVolt.totalSupply(); - -// uint256 oldVoltBalanceBefore = oldVolt.balanceOf(address(this)); -// uint256 newVoltBalanceBefore = volt.balanceOf(address(this)); - -// voltMigrator.exchangeAllTo(address(this)); - -// uint256 oldVoltBalanceAfter = oldVolt.balanceOf(address(this)); -// uint256 newVoltBalanceAfter = volt.balanceOf(address(this)); - -// assertEq( -// newVoltBalanceAfter, -// newVoltBalanceBefore + amountOldVoltToExchange -// ); -// assertEq( -// oldVoltBalanceAfter, -// oldVoltBalanceBefore - amountOldVoltToExchange -// ); - -// assertEq( -// oldVolt.totalSupply(), -// oldVoltTotalSupply - amountOldVoltToExchange -// ); -// assertEq(volt.totalSupply(), newVoltTotalSupply); - -// assertEq(oldVoltBalanceAfter, oldVoltBalanceBefore / 2); -// } - -// function testExchangeAllToPartialApproval() public { -// uint256 amountOldVoltToExchange = mintAmount / 2; // exchange half of users balance -// oldVolt.approve(address(voltMigrator), amountOldVoltToExchange); - -// vm.prank(grlm); -// volt.mint(address(voltMigrator), mintAmount); - -// vm.prank(multisig); -// CoreV2(coreV1).grantMinter(address(this)); -// oldVolt.mint(address(this), mintAmount); - -// uint256 newVoltTotalSupply = volt.totalSupply(); -// uint256 oldVoltTotalSupply = oldVolt.totalSupply(); - -// uint256 oldVoltBalanceBefore = oldVolt.balanceOf(address(this)); -// uint256 newVoltBalanceBefore = volt.balanceOf(address(0xFFF)); - -// voltMigrator.exchangeAllTo(address(0xFFF)); - -// uint256 oldVoltBalanceAfter = oldVolt.balanceOf(address(this)); -// uint256 newVoltBalanceAfter = volt.balanceOf(address(0xFFF)); - -// assertEq( -// newVoltBalanceAfter, -// newVoltBalanceBefore + amountOldVoltToExchange -// ); -// assertEq( -// oldVoltBalanceAfter, -// oldVoltBalanceBefore - amountOldVoltToExchange -// ); -// assertEq( -// oldVolt.totalSupply(), -// oldVoltTotalSupply - amountOldVoltToExchange -// ); -// assertEq(volt.totalSupply(), newVoltTotalSupply); -// assertEq(oldVoltBalanceAfter, oldVoltBalanceBefore / 2); -// } - -// function testSweep() public { -// uint256 amountToTransfer = 1_000_000e6; - -// uint256 startingBalance = usdc.balanceOf(address(timelockController)); - -// deal(address(usdc), address(voltMigrator), amountToTransfer); - -// vm.prank(multisig); -// voltMigrator.sweep( -// address(usdc), -// address(timelockController), -// amountToTransfer -// ); - -// uint256 endingBalance = usdc.balanceOf(address(timelockController)); - -// assertEq(endingBalance - startingBalance, amountToTransfer); -// } - -// function testSweepNonGovernorFails() public { -// uint256 amountToTransfer = 1_000_000e6; - -// deal(address(usdc), address(voltMigrator), amountToTransfer); - -// vm.expectRevert("CoreRef: Caller is not a governor"); -// voltMigrator.sweep( -// address(usdc), -// address(timelockController), -// amountToTransfer -// ); -// } - -// function testSweepNewVoltFails() public { -// uint256 amountToSweep = volt.balanceOf(address(voltMigrator)); - -// vm.prank(multisig); -// vm.expectRevert("VoltMigrator: cannot sweep new Volt"); -// voltMigrator.sweep( -// address(volt), -// address(timelockController), -// amountToSweep -// ); -// } - -// function testRedeemUsdc(uint72 amountVoltIn) public { -// oldVolt.approve(address(migratorRouter), type(uint256).max); - -// vm.prank(grlm); -// volt.mint(address(voltMigrator), amountVoltIn); - -// vm.prank(multisig); -// CoreV2(coreV1).grantMinter(address(this)); -// oldVolt.mint(address(this), amountVoltIn); - -// uint256 startBalance = usdc.balanceOf(address(this)); -// uint256 minAmountOut = usdcpsm.getRedeemAmountOut(amountVoltIn); - -// deal(address(usdc), address(usdcpsm), minAmountOut); - -// uint256 currentPegPrice = vso.getCurrentOraclePrice() / 1e12; -// uint256 amountOut = (amountVoltIn * currentPegPrice) / 1e18; - -// uint256 redeemedAmount = migratorRouter.redeemUSDC( -// amountVoltIn, -// minAmountOut -// ); -// uint256 endBalance = usdc.balanceOf(address(this)); - -// assertApproxEq(minAmountOut.toInt256(), amountOut.toInt256(), 0); -// assertEq(minAmountOut, endBalance - startBalance); -// assertEq(redeemedAmount, minAmountOut); -// } - -// function testRedeemDai(uint72 amountVoltIn) public { -// oldVolt.approve(address(migratorRouter), type(uint256).max); - -// vm.prank(grlm); -// volt.mint(address(voltMigrator), amountVoltIn); - -// vm.prank(multisig); -// CoreV2(coreV1).grantMinter(address(this)); -// oldVolt.mint(address(this), amountVoltIn); - -// uint256 startBalance = dai.balanceOf(address(this)); -// uint256 minAmountOut = daipsm.getRedeemAmountOut(amountVoltIn); - -// deal(address(dai), address(daipsm), minAmountOut); - -// uint256 currentPegPrice = vso.getCurrentOraclePrice(); -// uint256 amountOut = (amountVoltIn * currentPegPrice) / 1e18; - -// uint256 redeemedAmount = migratorRouter.redeemDai( -// amountVoltIn, -// minAmountOut -// ); -// uint256 endBalance = dai.balanceOf(address(this)); - -// assertApproxEq(minAmountOut.toInt256(), amountOut.toInt256(), 0); -// assertEq(minAmountOut, endBalance - startBalance); -// assertEq(redeemedAmount, minAmountOut); -// } - -// function testRedeemDaiFailsUserNotEnoughVolt() public { -// oldVolt.approve(address(migratorRouter), type(uint256).max); - -// uint256 minAmountOut = daipsm.getRedeemAmountOut(mintAmount); - -// vm.expectRevert("ERC20: transfer amount exceeds balance"); -// migratorRouter.redeemDai(mintAmount, minAmountOut); -// } - -// function testRedeemUsdcFailsUserNotEnoughVolt() public { -// oldVolt.approve(address(migratorRouter), type(uint256).max); - -// uint256 minAmountOut = daipsm.getRedeemAmountOut(mintAmount); - -// vm.expectRevert("ERC20: transfer amount exceeds balance"); -// migratorRouter.redeemUSDC(mintAmount, minAmountOut); -// } - -// function testRedeemDaiFailUnderfundedPSM() public { -// oldVolt.approve(address(migratorRouter), type(uint256).max); - -// vm.prank(grlm); -// volt.mint(address(voltMigrator), mintAmount); - -// vm.prank(multisig); -// CoreV2(coreV1).grantMinter(address(this)); -// oldVolt.mint(address(this), mintAmount); - -// uint256 balance = dai.balanceOf(address(daipsm)); -// vm.prank(address(daipsm)); -// dai.transfer(address(0), balance); - -// uint256 minAmountOut = daipsm.getRedeemAmountOut(mintAmount); - -// vm.expectRevert("Dai/insufficient-balance"); -// migratorRouter.redeemDai(mintAmount, minAmountOut); -// } - -// function testRedeemUsdcFailUnderfundedPSM() public { -// oldVolt.approve(address(migratorRouter), type(uint256).max); - -// vm.prank(grlm); -// volt.mint(address(voltMigrator), mintAmount); - -// vm.prank(multisig); -// CoreV2(coreV1).grantMinter(address(this)); -// oldVolt.mint(address(this), mintAmount); - -// uint256 balance = usdc.balanceOf(address(usdcpsm)); -// vm.prank(address(usdcpsm)); -// usdc.transfer(address(1), balance); - -// uint256 minAmountOut = usdcpsm.getRedeemAmountOut(mintAmount); - -// vm.expectRevert("ERC20: transfer amount exceeds balance"); -// migratorRouter.redeemUSDC(mintAmount, minAmountOut); -// } - -// function testRedeemDaiFailNoUserApproval() public { -// vm.prank(multisig); -// CoreV2(coreV1).grantMinter(address(this)); -// oldVolt.mint(address(this), mintAmount); - -// vm.prank(grlm); -// volt.mint(address(voltMigrator), mintAmount); - -// uint256 minAmountOut = daipsm.getRedeemAmountOut(mintAmount); - -// vm.expectRevert("ERC20: transfer amount exceeds allowance"); -// migratorRouter.redeemDai(mintAmount, minAmountOut); -// } - -// function testRedeemUsdcFailNoUserApproval() public { -// vm.prank(multisig); -// CoreV2(coreV1).grantMinter(address(this)); -// oldVolt.mint(address(this), mintAmount); - -// vm.prank(grlm); -// volt.mint(address(voltMigrator), mintAmount); - -// uint256 minAmountOut = daipsm.getRedeemAmountOut(mintAmount); - -// vm.expectRevert("ERC20: transfer amount exceeds allowance"); -// migratorRouter.redeemUSDC(mintAmount, minAmountOut); -// } - -// function testRedeemDaiFailUnderfundedMigrator() public { -// oldVolt.approve(address(migratorRouter), type(uint256).max); - -// vm.prank(multisig); -// CoreV2(coreV1).grantMinter(address(this)); -// oldVolt.mint(address(this), mintAmount); - -// uint256 minAmountOut = usdcpsm.getRedeemAmountOut(mintAmount); - -// vm.expectRevert(stdError.arithmeticError); -// migratorRouter.redeemDai(mintAmount, minAmountOut); -// } - -// function testRedeemUsdcFailUnderfundedMigrator() public { -// oldVolt.approve(address(migratorRouter), type(uint256).max); - -// vm.prank(multisig); -// CoreV2(coreV1).grantMinter(address(this)); -// oldVolt.mint(address(this), mintAmount); - -// uint256 minAmountOut = usdcpsm.getRedeemAmountOut(mintAmount); - -// vm.expectRevert(stdError.arithmeticError); -// migratorRouter.redeemUSDC(mintAmount, minAmountOut); -// } -// } diff --git a/test/integration/proposal-checks/IntegrationTestProposalPSMOracle.sol b/test/integration/proposal-checks/IntegrationTestProposalPSMOracle.sol index eef9e25d4..84c016e6c 100644 --- a/test/integration/proposal-checks/IntegrationTestProposalPSMOracle.sol +++ b/test/integration/proposal-checks/IntegrationTestProposalPSMOracle.sol @@ -99,7 +99,7 @@ contract IntegrationTestProposalPSMOracle is Test { addresses = proposals.addresses(); // get post-proposal addresses // Use post-proposal contracts if they have been migrated - psm = IPegStabilityModule(addresses.mainnet("PSM_USDC")); + psm = IPegStabilityModule(addresses.mainnet("PSM_NONCUSTODIAL_USDC")); volt = IERC20(addresses.mainnet("VOLT")); token = IERC20(addresses.mainnet("USDC")); diff --git a/test/proposals/vips/vip16.sol b/test/proposals/vips/vip16.sol index 6146b6fe7..86fd131cb 100644 --- a/test/proposals/vips/vip16.sol +++ b/test/proposals/vips/vip16.sol @@ -21,7 +21,6 @@ import {SystemEntry} from "@voltprotocol/entry/SystemEntry.sol"; import {PCVGuardian} from "@voltprotocol/pcv/PCVGuardian.sol"; import {IOracleRefV2} from "@voltprotocol/refs/IOracleRefV2.sol"; import {IPCVDepositV2} from "@voltprotocol/pcv/IPCVDepositV2.sol"; -import {MigratorRouter} from "@voltprotocol/v1-migration/MigratorRouter.sol"; import {NonCustodialPSM} from "@voltprotocol/peg/NonCustodialPSM.sol"; import {MakerPCVSwapper} from "@voltprotocol/pcv/maker/MakerPCVSwapper.sol"; import {VoltSystemOracle} from "@voltprotocol/oracle/VoltSystemOracle.sol"; @@ -31,7 +30,6 @@ import {MorphoAavePCVDeposit} from "@voltprotocol/pcv/morpho/MorphoAavePCVDeposi import {IPegStabilityModule} from "@voltprotocol/peg/IPegStabilityModule.sol"; import {ConstantPriceOracle} from "@voltprotocol/oracle/ConstantPriceOracle.sol"; import {CompoundBadDebtSentinel} from "@voltprotocol/pcv/compound/CompoundBadDebtSentinel.sol"; -import {IVoltMigrator, VoltMigrator} from "@voltprotocol/v1-migration/VoltMigrator.sol"; import {IGlobalReentrancyLock, GlobalReentrancyLock} from "@voltprotocol/core/GlobalReentrancyLock.sol"; import {IGlobalRateLimitedMinter, GlobalRateLimitedMinter} from "@voltprotocol/rate-limits/GlobalRateLimitedMinter.sol"; @@ -266,31 +264,6 @@ contract vip16 is Proposal { ); } - /// Volt Migration - { - VoltMigrator voltMigrator = new VoltMigrator( - addresses.mainnet("CORE"), - IVolt(addresses.mainnet("VOLT")) - ); - - /// TODO revisit this - // MigratorRouter migratorRouter = new MigratorRouter( - // IVolt(addresses.mainnet("VOLT")), - // IVoltMigrator(address(voltMigrator)), - // IPegStabilityModule(addresses.mainnet("PSM_DAI")), - // IPegStabilityModule(addresses.mainnet("PSM_USDC")) - // ); - - addresses.addMainnet( - "V1_MIGRATION_MIGRATOR", - address(voltMigrator) - ); - // addresses.addMainnet( - // "V1_MIGRATION_ROUTER", - // address(migratorRouter) - // ); - } - /// PCV Movement { SystemEntry systemEntry = new SystemEntry( @@ -406,10 +379,7 @@ contract vip16 is Proposal { core.createRole(VoltRoles.PCV_MOVER, VoltRoles.GOVERNOR); core.grantRole(VoltRoles.PCV_MOVER, addresses.mainnet("GOVERNOR")); /// team multisig - core.createRole( - VoltRoles.PSM_MINTER, - VoltRoles.GOVERNOR - ); + core.createRole(VoltRoles.PSM_MINTER, VoltRoles.GOVERNOR); core.grantRole( VoltRoles.PSM_MINTER, addresses.mainnet("PSM_NONCUSTODIAL_DAI") @@ -530,9 +500,6 @@ contract vip16 is Proposal { CoreV2 core = CoreV2(addresses.mainnet("CORE")); PCVOracle pcvOracle = PCVOracle(addresses.mainnet("PCV_ORACLE")); PCVRouter pcvRouter = PCVRouter(addresses.mainnet("PCV_ROUTER")); - MigratorRouter migratorRouter = MigratorRouter( - addresses.mainnet("V1_MIGRATION_ROUTER") - ); VoltSystemOracle oracle = VoltSystemOracle( addresses.mainnet("VOLT_SYSTEM_ORACLE") ); @@ -625,12 +592,6 @@ contract vip16 is Proposal { ), address(core) ); - assertEq( - address( - CoreRefV2(addresses.mainnet("V1_MIGRATION_MIGRATOR")).core() - ), - address(core) - ); // V1_MIGRATION_ROUTER is not CoreRef, it is only a util contract to call other contracts assertEq( address(CoreRefV2(addresses.mainnet("SYSTEM_ENTRY")).core()), @@ -681,7 +642,6 @@ contract vip16 is Proposal { ); assertEq(address(core.pcvOracle()), addresses.mainnet("PCV_ORACLE")); - // pcv oracle assertEq(pcvOracle.getVenues().length, 6); assertEq( @@ -716,21 +676,19 @@ contract vip16 is Proposal { // oracle references assertEq( - address(IOracleRefV2(addresses.mainnet("PSM_NONCUSTODIAL_DAI")).oracle()), + address( + IOracleRefV2(addresses.mainnet("PSM_NONCUSTODIAL_DAI")).oracle() + ), addresses.mainnet("VOLT_SYSTEM_ORACLE") ); assertEq( - address(IOracleRefV2(addresses.mainnet("PSM_NONCUSTODIAL_USDC")).oracle()), + address( + IOracleRefV2(addresses.mainnet("PSM_NONCUSTODIAL_USDC")) + .oracle() + ), addresses.mainnet("VOLT_SYSTEM_ORACLE") ); - // volt v1 migration references - assertEq(address(migratorRouter.newVolt()), addresses.mainnet("VOLT")); - assertEq( - address(migratorRouter.OLD_VOLT()), - addresses.mainnet("V1_VOLT") - ); - /// compound bad debt sentinel assertTrue( badDebtSentinel.isCompoundPcvDeposit( From 8e171046a451f77b0cc1b375dd7414a0beeaa096 Mon Sep 17 00:00:00 2001 From: Elliot Date: Mon, 27 Feb 2023 17:16:29 -0700 Subject: [PATCH 62/74] update circleci config --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index c87cb12cd..a3e6069aa 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -48,7 +48,7 @@ jobs: command: echo "export PATH=$PATH:$(pwd)/.foundry/bin" >> /home/circleci/.bashrc - run: name: Install Foundry - command: curl -L https://foundry.paradigm.xyz | bash; source /home/circleci/.bashrc; $HOME/.foundry/bin/foundryup + command: curl -L https://foundry.paradigm.xyz | bash; source /home/circleci/.bashrc; foundryup -b master - run: name: Run tests command: | From fb0a0ca0bcacc2d0213e71a1e6f3f79b6ebe2dcf Mon Sep 17 00:00:00 2001 From: Elliot Date: Mon, 27 Feb 2023 17:18:52 -0700 Subject: [PATCH 63/74] foundryup master branch for circleci --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index a3e6069aa..a989eb3d2 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -48,7 +48,7 @@ jobs: command: echo "export PATH=$PATH:$(pwd)/.foundry/bin" >> /home/circleci/.bashrc - run: name: Install Foundry - command: curl -L https://foundry.paradigm.xyz | bash; source /home/circleci/.bashrc; foundryup -b master + command: curl -L https://foundry.paradigm.xyz | bash; source /home/circleci/.bashrc; $HOME/.foundry/bin/foundryup -b master - run: name: Run tests command: | From bdd1d0cde59d634ccc6caf27afcfd88f5ef85f54 Mon Sep 17 00:00:00 2001 From: Elliot Date: Mon, 27 Feb 2023 17:26:32 -0700 Subject: [PATCH 64/74] isolate foundry setup actions --- .circleci/config.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index a989eb3d2..5a721c280 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -46,9 +46,15 @@ jobs: - run: name: Finish setting up env command: echo "export PATH=$PATH:$(pwd)/.foundry/bin" >> /home/circleci/.bashrc + - run: + name: Download Foundry + command: curl -L https://foundry.paradigm.xyz | bash; + - run: + name: Foundry Environment Variables + command: source /home/circleci/.bashrc && export PATH=$PATH:$HOME/.foundry/bin - run: name: Install Foundry - command: curl -L https://foundry.paradigm.xyz | bash; source /home/circleci/.bashrc; $HOME/.foundry/bin/foundryup -b master + command: foundryup -b master - run: name: Run tests command: | From 86ae88f30af61ee308afdbb2fc80b07e259aa6b0 Mon Sep 17 00:00:00 2001 From: Elliot Date: Mon, 27 Feb 2023 17:28:24 -0700 Subject: [PATCH 65/74] hardcode path to foundryup --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 5a721c280..760bcfa80 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -54,7 +54,7 @@ jobs: command: source /home/circleci/.bashrc && export PATH=$PATH:$HOME/.foundry/bin - run: name: Install Foundry - command: foundryup -b master + command: $HOME/.foundry/bin/foundryup -b master - run: name: Run tests command: | From 0e52ef148db3a37c60db82659953d78a95c8e66a Mon Sep 17 00:00:00 2001 From: Elliot Date: Mon, 27 Feb 2023 17:32:52 -0700 Subject: [PATCH 66/74] revert ci --- .circleci/config.yml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 760bcfa80..7b7db3854 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -48,13 +48,7 @@ jobs: command: echo "export PATH=$PATH:$(pwd)/.foundry/bin" >> /home/circleci/.bashrc - run: name: Download Foundry - command: curl -L https://foundry.paradigm.xyz | bash; - - run: - name: Foundry Environment Variables - command: source /home/circleci/.bashrc && export PATH=$PATH:$HOME/.foundry/bin - - run: - name: Install Foundry - command: $HOME/.foundry/bin/foundryup -b master + command: curl -L https://foundry.paradigm.xyz | bash; source /home/circleci/.bashrc; $HOME/.foundry/bin/foundryup - run: name: Run tests command: | From 072e4e6ff45f3d2f8a4356f3e72f546caae4585c Mon Sep 17 00:00:00 2001 From: Elliot Date: Mon, 27 Feb 2023 17:40:07 -0700 Subject: [PATCH 67/74] remove cache --- .circleci/config.yml | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 7b7db3854..203a0ea85 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -13,10 +13,10 @@ jobs: - run: name: Install dependencies command: npm install - - save_cache: - key: repo-{{ .Environment.CIRCLE_SHA1 }} - paths: - - ~/repo + # - save_cache: + # key: repo-{{ .Environment.CIRCLE_SHA1 }} + # paths: + # - ~/repo lint: working_directory: ~/repo @@ -37,9 +37,9 @@ jobs: resource_class: xlarge steps: - checkout - - restore_cache: - keys: - - repo-{{ .Environment.CIRCLE_SHA1 }} + # - restore_cache: + # keys: + # - repo-{{ .Environment.CIRCLE_SHA1 }} - run: name: Setup env command: echo "export PATH=$PATH:$(pwd)/.circleci" >> /home/circleci/.bashrc @@ -49,6 +49,9 @@ jobs: - run: name: Download Foundry command: curl -L https://foundry.paradigm.xyz | bash; source /home/circleci/.bashrc; $HOME/.foundry/bin/foundryup + - run: + name: Debug Foundry Version + command: $HOME/.foundry/bin/foundry -V - run: name: Run tests command: | From 17243853935faf042c5db253ea9783d4a672079a Mon Sep 17 00:00:00 2001 From: Elliot Date: Tue, 28 Feb 2023 18:16:22 -0700 Subject: [PATCH 68/74] revert ci config --- .circleci/config.yml | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 203a0ea85..7b7db3854 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -13,10 +13,10 @@ jobs: - run: name: Install dependencies command: npm install - # - save_cache: - # key: repo-{{ .Environment.CIRCLE_SHA1 }} - # paths: - # - ~/repo + - save_cache: + key: repo-{{ .Environment.CIRCLE_SHA1 }} + paths: + - ~/repo lint: working_directory: ~/repo @@ -37,9 +37,9 @@ jobs: resource_class: xlarge steps: - checkout - # - restore_cache: - # keys: - # - repo-{{ .Environment.CIRCLE_SHA1 }} + - restore_cache: + keys: + - repo-{{ .Environment.CIRCLE_SHA1 }} - run: name: Setup env command: echo "export PATH=$PATH:$(pwd)/.circleci" >> /home/circleci/.bashrc @@ -49,9 +49,6 @@ jobs: - run: name: Download Foundry command: curl -L https://foundry.paradigm.xyz | bash; source /home/circleci/.bashrc; $HOME/.foundry/bin/foundryup - - run: - name: Debug Foundry Version - command: $HOME/.foundry/bin/foundry -V - run: name: Run tests command: | From ae7083afdb47ae6c39bbcd6315c224b78582e5b4 Mon Sep 17 00:00:00 2001 From: Elliot Date: Wed, 1 Mar 2023 11:35:52 -0700 Subject: [PATCH 69/74] remove market governance code --- src/utils/DeviationWeiGranularity.sol | 23 - src/vcon/IMarketGovernance.sol | 126 --- src/vcon/MarketGovernance.sol | 649 ------------ .../IntegrationTestEulerPCVDeposit.sol | 1 - test/unit/utils/Deviation.sol | 11 - test/unit/utils/RateLimitedV2.t.sol | 2 - test/unit/vcon/MarketGovernance.t.sol | 950 ------------------ 7 files changed, 1762 deletions(-) delete mode 100644 src/utils/DeviationWeiGranularity.sol delete mode 100644 src/vcon/IMarketGovernance.sol delete mode 100644 src/vcon/MarketGovernance.sol delete mode 100644 test/unit/vcon/MarketGovernance.t.sol diff --git a/src/utils/DeviationWeiGranularity.sol b/src/utils/DeviationWeiGranularity.sol deleted file mode 100644 index c237b548d..000000000 --- a/src/utils/DeviationWeiGranularity.sol +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.13; - -import {Constants} from "@voltprotocol/Constants.sol"; -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; - -/// @title contract that determines whether or not a new value is within -/// an acceptable deviation threshold -/// @author Elliot Friedman -library DeviationWeiGranularity { - using SafeCast for *; - - /// @notice return the percent deviation between a and b in wei. 1 eth = 100% - function calculateDeviation( - int256 a, - int256 b - ) internal pure returns (int256) { - int256 delta = a - b; - int256 basisPoints = (delta * Constants.ETH_GRANULARITY_INT) / a; - - return basisPoints; - } -} diff --git a/src/vcon/IMarketGovernance.sol b/src/vcon/IMarketGovernance.sol deleted file mode 100644 index 1b7e3155c..000000000 --- a/src/vcon/IMarketGovernance.sol +++ /dev/null @@ -1,126 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.13; - -interface IMarketGovernance { - /// @notice struct that tracks a PCV movement - /// @param source pcv deposit to pull funds from - /// @param destination pcv deposit to send funds - /// @param swapper address to swap tokens - /// @param amountPcv to move from src to dest - struct Rebalance { - address source; - address destination; - address swapper; - uint256 amountPcv; - } - - struct PCVDepositInfo { - address deposit; - uint256 amount; - } - - /// ---------- Events ---------- - - /// @notice event emitted when a user stakes their VCON - event VconStaked( - address indexed user, - uint256 timestamp, - uint256 vconAmount - ); - - /// @notice event emitted when a user unstakes their VCON - event VconUnstaked( - address indexed user, - uint256 timestamp, - uint256 vconAmount - ); - - /// @notice emitted when the router is updated - event PCVRouterUpdated( - address indexed oldPcvRouter, - address indexed newPcvRouter - ); - - /// @notice emitted when profit to vcon ratio is updated - event ProfitToVconRatioUpdated( - address indexed venue, - uint256 oldRatio, - uint256 newRatio - ); - - /// @notice emitted when a loss is realized - event LossRealized( - address indexed venue, - address indexed user, - uint256 vconLossAmount - ); - - /// @notice emitted whenever a user stakes - event Staked( - address indexed venue, - address indexed user, - uint256 vconAmount - ); - - /// @notice emitted whenever a user unstakes - event Unstaked( - address indexed venue, - address indexed user, - uint256 vconAmount, - uint256 pcvAmount - ); - - /// @notice emitted whenever a user harvests - /// vcon will be negative if a loss is realized - event Harvest( - address indexed venue, - address indexed user, - int256 vconAmount - ); - - /// @notice emitted when a venue's index is updated via accrue - event VenueIndexUpdated( - address indexed venue, - uint256 indexed timestamp, - int256 profitIndex - ); - - /// @notice emitted when the safe address for a given token denomination is updated - event UnderlyingTokenDepositUpdated( - address indexed token, - address indexed venue - ); - - /// @notice emitted when share price is marked down through governance - event LossesApplied( - address indexed venue, - uint256 oldSharePrice, - uint256 newSharePrice - ); - - /// ---------- Permissionless User PCV Allocation Methods ---------- - - /// stake VCON on a venue - /// @param amountVcon to stake - /// @param venue pcv deposit to pull funds from - function stake(uint256 amountVcon, address venue) external; - - /// @notice this function automatically calculates - /// the amount of PCV to remove from the venue - /// based on the user's total amount of staked VCON - /// @param amountVcon to stake - /// @param venue pcv deposit to pull funds from - /// @param vconRecipient address to receive VCON tokens - function unstake( - uint256 amountVcon, - address venue, - address vconRecipient - ) external; - - /// @notice permissionlessly rebalance PCV based on - /// the pre-existing VCON weights. Each movement must make - /// the system more balanced, otherwise it will revert - /// causing the entire transaction to fail. - /// @param movements of PCV between venues - function rebalance(Rebalance[] calldata movements) external; -} diff --git a/src/vcon/MarketGovernance.sol b/src/vcon/MarketGovernance.sol deleted file mode 100644 index d4fd91876..000000000 --- a/src/vcon/MarketGovernance.sol +++ /dev/null @@ -1,649 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity =0.8.13; - -import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; - -import {Constants} from "@voltprotocol/Constants.sol"; -import {PCVRouter} from "@voltprotocol/pcv/PCVRouter.sol"; -import {CoreRefV2} from "@voltprotocol/refs/CoreRefV2.sol"; -import {IPCVOracle} from "@voltprotocol/oracle/IPCVOracle.sol"; -import {IPCVDeposit} from "@voltprotocol/pcv/IPCVDeposit.sol"; -import {IPCVDepositV2} from "@voltprotocol/pcv/IPCVDepositV2.sol"; -import {IMarketGovernance} from "@voltprotocol/vcon/IMarketGovernance.sol"; -import {DeviationWeiGranularity} from "@voltprotocol/utils/DeviationWeiGranularity.sol"; - -import {console} from "@forge-std/console.sol"; - -/// TODO cache pcv oracle total pcv so getAllPCV isn't needed to figure -/// out total pcv - -/// @notice this contract requires the PCV Mover and Locker role -/// -/// If an account has an unrealized loss on a venue, they cannot do any other action on that venue -/// until they have called the function realizeLosses and marked down the amount of VCON they have staked -/// on that venue. Once the loss has been marked down, they can proceed with other actions. -/// -/// The VCON:Dollar ratio is the same for both profits and losses. If a venue has a VCON:Dollar ratio of 5:1 -/// and the venue gains $5 in profits, then 25 VCON will be distributed across all VCON stakers in that venue. -/// If that same venue losses $5, then a loss of 25 VCON will be distributed across all VCON stakers in that venue. -/// -/// @dev this contract assumes it is already topped up with the VCON necessary to pay rewards. -/// A dripper will keep this contract funded at a steady pace. -/// -/// three main data points are tracked in each venue. -/// 1. the last recorded profit in a given venue. this tracks the last recorded profit amount in the underlying venue. -/// 2. the vcon share price in a given venue. this applies the last recorded profit across all VCON stakers in the -/// venue evenly and ensures that each user receives their pro-rata share of rewards. -/// 3. profit to vcon ratio in each venue. this tracks the profit to vcon ratio for each venue and is used to calculate -/// the vcon share price by finding the new last recorded profit, and finding the profit delta -/// vcon share price formula -/// -/// Formula for market governance rewards share price: -/// ∆Cumulative Profits (Dollars) = currentProfits - lastRecordedProfits -/// Profit Per VCON = Profit Per VCON + ∆Cumulative Profits (Dollars) * VCON:Dollar Ratio / Venue VCON Staked -/// -/// Formula for calculating user VCON rewards: -/// ∆Share Price = (Ending Share Price - Starting Share Price) -/// User VCON rewards = ∆Share Price * User Shares -/// -/// Anytime the rewards share price changes, so does the unclaimed user VCON rewards. -/// -/// @dev users can stake on PCV deposits that are not productive, such as the holding deposit, -/// however, they will receive no rewards for doing so if the profit to VCON ratio is not set. -/// The PSM will not be able to be staked on as it is not whitelisted in the PCV Oracle. -contract MarketGovernance is CoreRefV2, IMarketGovernance { - using DeviationWeiGranularity for *; - using SafeERC20 for *; - using SafeCast for *; - - /// @notice reference to the PCV Router - address public pcvRouter; - - /// @dev convention for all normal mappings is key (venue -> value) - - /// @notice amount of VCON paid per unit of revenue generated per venue - /// different venues may have different ratios to account for rewards - /// which will not be included in the V1 - mapping(address => uint256) public profitToVconRatio; - - /// and do the conversion to VCON at the end when accruing rewards - /// pack venueLastRecordedProfit, venueTotalShares and profitToVconRatio - /// into a single slot for gas optimization - - /// @notice last recorded profit index per venue - mapping(address => int128) public venueLastRecordedProfit; - - /// @notice last recorded VCON share price index per venue - mapping(address => uint128) public venueLastRecordedVconSharePrice; - - /// @notice total vcon deposited per venue - mapping(address => uint256) public venueTotalShares; - - /// @notice map an underlying token to the corresponding holding deposit - /// TODO remove this and all related logic once the PCV Guardian is re-written - /// instead add a reference to the PCV Guardian - mapping(address => address) public underlyingTokenToHoldingDeposit; - - /// ---------- Per Venue User Profit Tracking ---------- - - /// @dev convention for all double nested address mappings is key (venue -> user) -> value - - /// @notice record how much VCON a user deposited in a given venue - mapping(address => mapping(address => uint256)) public venueUserShares; - - /// @param _core reference to core - /// @param _pcvRouter reference to pcvRouter - constructor(address _core, address _pcvRouter) CoreRefV2(_core) { - pcvRouter = _pcvRouter; - } - - /// @notice update the VCON share price and the last recorded profit for a given venue - /// @param venue address to accrue - function accrueVcon(address venue) external globalLock(1) whenNotPaused { - IPCVOracle oracle = pcvOracle(); - - require(oracle.isVenue(venue), "MarketGovernance: invalid destination"); - - _accrue(venue); - } - - /// ---------- Permissionless User PCV Allocation Methods ---------- - - /// @notice a user can get slashed up to their full VCON stake for entering - /// a venue that takes a loss. - /// @param amountVcon to stake on destination - /// @param venue address to accrue rewards to, and send funds to - function stake( - uint256 amountVcon, - address venue - ) external globalLock(1) whenNotPaused { - IPCVOracle oracle = pcvOracle(); - require(oracle.isVenue(venue), "MarketGovernance: invalid destination"); - - _accrue(venue); /// update share price in the destination so the user gets in at the current share price - - /// vconToShares will always return correctly as accrue will set venueLastRecordedVconSharePrice - /// to the correct share price from 0 if uninitialized - uint256 userShareAmount = vconToShares(venue, amountVcon); - - /// user updates - venueUserShares[venue][msg.sender] += userShareAmount; - - /// venue updates - venueTotalShares[venue] += userShareAmount; - - /// check and an interaction with a trusted contract - vcon().safeTransferFrom(msg.sender, address(this), amountVcon); /// transfer VCON in - - emit Staked(venue, msg.sender, amountVcon); - } - - /// @notice unstake VCON and transfer corresponding VCON to another venue - /// @param shareAmount the amount of shares to unstake - /// @param venue address to accrue rewards to, and pull funds from - /// @param vconRecipient address to receive the VCON - /// @dev both venue and destination are checked twice, - /// the first time in the market governance contract, - /// the second time in the PCV Router contract. - function unstake( - uint256 shareAmount, - address venue, - address vconRecipient - ) external globalLock(1) whenNotPaused { - /// ---------- Checks ---------- - IPCVOracle oracle = pcvOracle(); - - require(oracle.isVenue(venue), "MarketGovernance: invalid venue"); - require( - venueUserShares[venue][msg.sender] >= shareAmount, - "MarketGovernance: invalid share amount" - ); - - address denomination = IPCVDepositV2(venue).token(); - address destination = underlyingTokenToHoldingDeposit[denomination]; - require( - destination != address(0), - "MarketGovernance: invalid destination" - ); - - /// ---------- Effects ---------- - - _accrue(venue); /// update profitPerVCON in the venue so the user gets paid out at the current share price - - /// figure out how balanced the system is before withdraw - - /// amount of PCV to withdraw is the amount vcon * venue balance / total vcon staked on venue - uint256 amountPcv = getProRataPCVAmounts(venue, shareAmount); - - /// user updates - venueUserShares[venue][msg.sender] -= shareAmount; - - /// venue updates - venueTotalShares[venue] -= shareAmount; - - /// ---------- Interactions ---------- - - { - PCVRouter(pcvRouter).movePCV( - venue, - destination, - address(0), - amountPcv, - denomination, - denomination - ); - } - - { - uint256 amountVcon = sharesToVcon(venue, shareAmount); - vcon().safeTransfer(vconRecipient, amountVcon); /// transfer VCON amount to recipient - emit Unstaked(venue, msg.sender, amountVcon, amountPcv); - } - } - - /// @notice rebalance PCV without staking or unstaking VCON - /// each individual action must make the system more balanced - /// as a whole, otherwise it will revert - /// @param movements information on all pcv movements - /// including sources, destinations, amounts and swappers - function rebalance( - Rebalance[] calldata movements - ) external globalLock(1) whenNotPaused { - /// read unsafe because we are at lock level 1 - IPCVOracle oracle = pcvOracle(); - uint256 totalPcv = oracle.getTotalPcv(); - uint256 totalVconStaked = getTotalVconStaked(); - unchecked { - for (uint256 i = 0; i < movements.length; i++) { - address source = movements[i].source; - address destination = movements[i].destination; - address swapper = movements[i].swapper; - uint256 amountPcv = movements[i].amountPcv; - - /// validate source, dest and swapper - require( - oracle.isVenue(source), - "MarketGovernance: invalid source" - ); - require( - oracle.isVenue(destination), - "MarketGovernance: invalid destination" - ); - - /// if swapper is used, validate in PCV Router whitelist - if (swapper != address(0)) { - require( - PCVRouter(pcvRouter).isPCVSwapper(swapper), - "MarketGovernance: invalid swapper" - ); - } - - /// call accrue on destination to ensure no unrealized losses have occured - _accrue(destination); - - /// record how balanced the system is before the PCV movement - int256 sourceVenueBalance = getVenueDeviation( - source, - totalPcv, - totalVconStaked - ); - int256 destinationVenueBalance = getVenueDeviation( - destination, - totalPcv, - totalVconStaked - ); - - _movePCVWithChecks( - source, - destination, - swapper, - amountPcv, - totalPcv, - sourceVenueBalance, - destinationVenueBalance, - totalVconStaked - ); - } - } - } - - /// ------------- View Only Methods ------------- - - /// @param venue to get share price from - /// @param shareAmount to withdraw - /// @return vconAmount the amount of VCON received for a given amount of shares - function sharesToVcon( - address venue, - uint256 shareAmount - ) public view returns (uint256 vconAmount) { - uint256 sharePrice = venueLastRecordedVconSharePrice[venue]; - - /// if share price is 0, accrueVcon must be called first - vconAmount = (sharePrice * shareAmount) / Constants.ETH_GRANULARITY; - } - - /// @param venue to get share price from - /// @param amountVcon to deposit - /// return the amount of shares received from depositing into a given venue - function vconToShares( - address venue, - uint256 amountVcon - ) public view returns (uint256 shareAmount) { - uint256 sharePrice = venueLastRecordedVconSharePrice[venue]; - - shareAmount = (amountVcon * Constants.ETH_GRANULARITY) / sharePrice; - } - - /// @notice returns the amount of VCON staked in a single venue - function getVenueVconStaked(address venue) public view returns (uint256) { - return - (venueTotalShares[venue] * venueLastRecordedVconSharePrice[venue]) / - Constants.ETH_GRANULARITY; - } - - /// get the total amount of VCON staked based on the last cached share prices of each venue - /// during a loss scenario, this function will return an incorrect total amount of VCON staked - /// because the share price in the venues have not been marked down, leading to an incorrect sum. - /// all functions that call this function during a loss scenario will also be incorrect - function getTotalVconStaked() - public - view - returns (uint256 totalVconStaked) - { - address[] memory pcvDeposits = pcvOracle().getVenues(); - uint256 totalVenues = pcvDeposits.length; - - for (uint256 i = 0; i < totalVenues; ) { - address venue = pcvDeposits[i]; - - totalVconStaked += getVenueVconStaked(venue); - unchecked { - i++; - } - } - } - - /// @notice returns positive value if over allocated - /// returns negative value if under allocated - /// if no venue balance and deposited vcon, return positive - /// if venue balance and no deposited vcon, return negative - /// - /// @param venue to query - /// @param totalPcv expected venue pcv - /// @param totalVconStaked expected venue pcv - function getVenueDeviation( - address venue, - uint256 totalPcv, - uint256 totalVconStaked - ) public view returns (int256) { - uint256 venueBalance = pcvOracle().getVenueBalance(venue); /// decimal normalized balance - uint256 expectedVenueBalance = getExpectedVenuePCVAmount( - venue, - totalPcv, - totalVconStaked - ); - - return venueBalance.toInt256() - expectedVenueBalance.toInt256(); - } - - /// @param venue to figure out total pro rata pcv - /// @param shareAmount to find total amount of pro rata pcv - /// @return the pro rata pcv controlled in the given venue based on the amount of shares - /// returned amount will be used to call the PCV router, so return a non-decimal normalized value - function getProRataPCVAmounts( - address venue, - uint256 shareAmount - ) public view returns (uint256) { - uint256 venuePcv = IPCVDepositV2(venue).balance(); - uint256 cachedTotalShares = venueTotalShares[venue]; /// save a single warm SLOAD - - /// 0 checks as any 0 denominator will cause a revert - if (cachedTotalShares == 0) { - return 0; - } - - /// @audit we do not add 1 to the pro rata PCV here. This means a withdrawal of 1 Wei of shares - /// will allow removing a user's VCON without having to withdraw PCV from a venue. - /// This is a known issue, however it is not harmful as it would require a quintillion withdrawals - /// to withdraw 1 VCON, which would cost at minimum 5,000e18 gas per withdraw, meaning it would cost at least 1 million ether - /// (likely more) to retrieve a single VCON without moving PCV. - /// The only reason this would ever get expoited is if a loss was taken and a user was trying to avoid realizing their portion - /// of the losses. However, in a loss scenario, the unstake function does not allow execution if the user has an unrealized - /// loss in that venue. This condition stops the aforementioned exploit. - /// fix would require rounding up in the protocol's favor, so that a withdrawal of 1 Wei of VCON has an actual withdraw amount - uint256 proRataPcv = (shareAmount * venuePcv) / cachedTotalShares; - - return proRataPcv; - } - - /// @dev algorithm to find all rebalance actions necessary to get system to fully balanced - /// get expected PCV amounts in each venue - /// build out 2 ordered arrays - /// 1 of deposits underweight with most underweight placed first - /// 2 of deposits overweight with most overweight placed first - /// create an array of actions - /// iterate through list 2, starting at item 0, then iterate over list 1, making an action - /// pulling funds from this venue either till exhausted, or until item in list one is filled - /// if list 1 item filled, iterate to next item in list 1 - /// if list 1 item unfilled and list 2 item perfectly balanced, go to next item in list 2 - /// iterate over array of actions and correct decimals if swapping between USDC and DAI - - /// @notice return what the perfectly balanced system would look like with all balances normalized to 1e18 - function getExpectedPCVAmounts() - external - view - returns (PCVDepositInfo[] memory deposits) - { - address[] memory pcvDeposits = pcvOracle().getVenues(); - uint256 totalVenues = pcvDeposits.length; - uint256 totalPcv = pcvOracle().getTotalPcv(); - uint256 cachedVconStaked = getTotalVconStaked(); /// Save repeated warm SLOADs - - deposits = new PCVDepositInfo[](totalVenues); - - unchecked { - for (uint256 i = 0; i < totalVenues; i++) { - address venue = pcvDeposits[i]; - deposits[i].deposit = venue; - deposits[i].amount = getExpectedVenuePCVAmount( - venue, - totalPcv, - cachedVconStaked - ); - } - } - } - - function getExpectedVenuePCVAmount( - address venue, - uint256 totalPcv, - uint256 totalVconStaked - ) public view returns (uint256 expectedPcvAmount) { - uint256 venueDepositedVcon = sharesToVcon( - venue, - venueTotalShares[venue] - ); - - if (totalVconStaked == 0) { - return 0; - } - - expectedPcvAmount = (venueDepositedVcon * totalPcv) / totalVconStaked; - } - - /// ------------- Helper Methods ------------- - - /// @param source address to pull funds from - /// @param destination recipient address for funds - /// @param swapper address to swap tokens with - /// @param amountPcv the amount of PCV to move from source - function _movePCVWithChecks( - address source, - address destination, - address swapper, - uint256 amountPcv, - uint256 totalPcv, - int256 sourceVenueBalance, - int256 destinationVenueBalance, - uint256 totalVconStaked - ) private { - address sourceAsset = IPCVDepositV2(source).token(); - address destinationAsset = IPCVDepositV2(destination).token(); - - /// validate pcv movement - /// check underlying assets match up and if not that swapper is provided and valid - PCVRouter(pcvRouter).movePCV( - source, - destination, - swapper, - amountPcv, - sourceAsset, - destinationAsset - ); - - /// if nothing is staked on source, ignore balance check - if (sharesToVcon(source, venueTotalShares[source]) > 0) { - int256 sourceVenueBalanceAfter = getVenueDeviation( - source, - totalPcv, - totalVconStaked - ); - - /// source and dest venue balance measures the distance from being perfectly balanced - - /// validate source venue balance became more balanced - _checkBalance( - sourceVenueBalance, - sourceVenueBalanceAfter, - "MarketGovernance: src more imbalanced" - ); - } - - int256 destinationVenueBalanceAfter = getVenueDeviation( - destination, - totalPcv, - totalVconStaked - ); - - /// validate destination venue balance became more balanced - _checkBalance( - destinationVenueBalance, - destinationVenueBalanceAfter, - "MarketGovernance: dest more imbalanced" - ); - } - - /// @notice helper function to validate balance moved in the right direction after a pcv movement - /// if balance before and after are the same, return true - function _checkBalance( - int256 balanceBefore, - int256 balanceAfter, - string memory reason - ) private pure { - require( - balanceBefore < 0 /// if balance is under weight relative to vcon staked, ensure it doesn't go over balance - ? balanceAfter >= balanceBefore && balanceAfter <= 0 /// if balance is over weight relative to vcon staked, ensure it doesn't go under balance - : balanceAfter <= balanceBefore && balanceAfter >= 0, - reason - ); - } - - /// @notice update the venue last recorded profit - /// and the venue last recorded vcon share price - /// system must be at lock level 1 to call this function, - /// otherwise call to `accrue()` on the PCV Deposit will fail - /// @dev this function assumes the venue is in the PCV Oracle as the - /// calling function should either check, or be callable only by - /// governance - function _accrue(address venue) private { - /// cache starting recorded profit before the external call even though - /// there is no way to call _accrue without setting the global reentrancy lock to level 1 - int256 startingLastRecordedProfit = venueLastRecordedProfit[venue]; - - IPCVDepositV2(venue).accrue(); - - int256 lastRecordedProfit = pcvOracle().lastRecordedProfit(venue); /// get this from the pcv oracle as it will be decimal normalized - uint256 endingLastRecordedSharePrice = venueLastRecordedVconSharePrice[ - venue - ]; - int256 venueProfit = lastRecordedProfit - startingLastRecordedProfit; - - require(venueProfit >= 0, "MarketGovernance: loss scenario"); /// TODO remove this - - /// update venue last recorded profit regardless - /// of participation in market governance - if (endingLastRecordedSharePrice != 0) { - uint256 venueShares = venueTotalShares[venue]; - - /// if venue has 0 staked vcon, do not update share price, just update profit index - if (venueShares != 0) { - uint256 venueProfitRatio = profitToVconRatio[venue]; - - uint256 vconEarnedPerShare = (Constants.ETH_GRANULARITY * - venueProfit.toUint256() * - venueProfitRatio) / venueShares; - - /// gain scenario - venueLastRecordedVconSharePrice[venue] += vconEarnedPerShare - .toUint128(); - } - } else { - /// share price is 0, and requires initialization - venueLastRecordedVconSharePrice[venue] = Constants - .ETH_GRANULARITY - .toUint128(); - } - - /// update the venue's profit index - venueLastRecordedProfit[venue] = lastRecordedProfit.toInt128(); - - emit VenueIndexUpdated(venue, block.timestamp, lastRecordedProfit); - } - - /// ---------- Governor-Only Permissioned API ---------- - - /// @notice governor only function to set the profit to VCON ratio - /// this function will not be callable if an underlying venue took a loss as `_accrue()` - /// will revert and no users will be able to withdraw their VCON. - function setProfitToVconRatio( - address venue, - uint256 newProfitToVconRatio - ) external onlyGovernor globalLock(1) { - /// lock to level 1 so that accrue can succeed - require(pcvOracle().isVenue(venue), "MarketGovernance: invalid venue"); - _accrue(venue); /// ensure users receive all rewards from the old rate - - uint256 oldProfitToVconRatio = profitToVconRatio[venue]; - profitToVconRatio[venue] = newProfitToVconRatio; - - emit ProfitToVconRatioUpdated( - venue, - oldProfitToVconRatio, - newProfitToVconRatio - ); - } - - /// @notice governor only function to set the PCV Router for market governance rebalances - function setPCVRouter(address newPcvRouter) external onlyGovernor { - address oldPcvRouter = pcvRouter; - pcvRouter = newPcvRouter; - - emit PCVRouterUpdated(oldPcvRouter, newPcvRouter); - } - - /// @notice governor only function to set a venue for market governance withdrawals in a given denomination - /// @param token underlying token for venue to handle - /// @param venue where funds of denomination `token` will be withdrawn to - /// @dev venue must be a whitelisted PCV Deposit, - /// and the venue's underlying token must match the token passed - function setUnderlyingTokenHoldingDeposit( - address token, - address venue - ) external onlyGovernor { - require(pcvOracle().isVenue(venue), "MarketGovernance: invalid venue"); - require( - IPCVDepositV2(venue).token() == token, - "MarketGovernance: underlying mismatch" - ); - - underlyingTokenToHoldingDeposit[token] = venue; - - emit UnderlyingTokenDepositUpdated(token, venue); - } - - /// Governor applies losses to a venue - /// @param venue address of venue to apply losses to - /// @param newSharePrice new share price - function applyVenueLosses( - address venue, - uint128 newSharePrice - ) external onlyGovernor globalLock(1) { - require(pcvOracle().isVenue(venue), "MarketGovernance: invalid venue"); - - uint256 oldSharePrice = venueLastRecordedVconSharePrice[venue]; - - /// setting to 0 would cause re-initialization in accrue function, nullifying these changes - require( - newSharePrice != 0, - "MarketGovernance: cannot set share price to 0" - ); - - /// cannot apply a gain to the share price, only losses - require( - newSharePrice < oldSharePrice, - "MarketGovernance: share price not less" - ); - - IPCVDepositV2(venue).accrue(); - - int128 lastRecordedProfit = pcvOracle().lastRecordedProfit(venue); - - /// update the venue's profit index - venueLastRecordedProfit[venue] = lastRecordedProfit; - /// update the venue's share price - venueLastRecordedVconSharePrice[venue] = newSharePrice; - - emit LossesApplied(venue, oldSharePrice, newSharePrice); - } -} diff --git a/test/integration/post-proposal-checks/IntegrationTestEulerPCVDeposit.sol b/test/integration/post-proposal-checks/IntegrationTestEulerPCVDeposit.sol index a111d54c7..372e0ada2 100644 --- a/test/integration/post-proposal-checks/IntegrationTestEulerPCVDeposit.sol +++ b/test/integration/post-proposal-checks/IntegrationTestEulerPCVDeposit.sol @@ -4,7 +4,6 @@ pragma solidity 0.8.13; import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {console} from "@forge-std/console.sol"; import {SystemEntry} from "@voltprotocol/entry/SystemEntry.sol"; import {IPCVDepositV2} from "@voltprotocol/pcv/IPCVDepositV2.sol"; import {EulerPCVDeposit} from "@voltprotocol/pcv/euler/EulerPCVDeposit.sol"; diff --git a/test/unit/utils/Deviation.sol b/test/unit/utils/Deviation.sol index bdfd952b3..c104c0e7f 100644 --- a/test/unit/utils/Deviation.sol +++ b/test/unit/utils/Deviation.sol @@ -49,17 +49,6 @@ library Deviation { return (basisPoints < 0 ? basisPoints * -1 : basisPoints).toUint256(); } - /// @notice return the percent deviation between a and b in basis points terms - function calculateDeviationBasisPoints( - int256 a, - int256 b - ) internal pure returns (int256) { - int256 delta = a - b; - int256 basisPoints = (delta * Constants.BP_INT) / a; - - return basisPoints; - } - /// @notice function to return whether or not the new price is within /// the acceptable deviation threshold function isWithinDeviationThreshold( diff --git a/test/unit/utils/RateLimitedV2.t.sol b/test/unit/utils/RateLimitedV2.t.sol index 03f689ed6..6fef32863 100644 --- a/test/unit/utils/RateLimitedV2.t.sol +++ b/test/unit/utils/RateLimitedV2.t.sol @@ -4,8 +4,6 @@ import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; -import {console} from "@forge-std/console.sol"; - import {Vm} from "@forge-std/Vm.sol"; import {Test} from "@forge-std/Test.sol"; import {ICoreV2} from "@voltprotocol/core/ICoreV2.sol"; diff --git a/test/unit/vcon/MarketGovernance.t.sol b/test/unit/vcon/MarketGovernance.t.sol deleted file mode 100644 index 3a8b65e22..000000000 --- a/test/unit/vcon/MarketGovernance.t.sol +++ /dev/null @@ -1,950 +0,0 @@ -pragma solidity 0.8.13; - -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -import {console} from "@forge-std/console.sol"; -import {MockERC20} from "@test/mock/MockERC20.sol"; -import {VoltRoles} from "@voltprotocol/core/VoltRoles.sol"; -import {Constants} from "@voltprotocol/Constants.sol"; -import {PCVRouter} from "@voltprotocol/pcv/PCVRouter.sol"; -import {MockPCVSwapper} from "@test/mock/MockPCVSwapper.sol"; -import {SystemUnitTest} from "@test/unit/system/System.t.sol"; -import {MockPCVDepositV3} from "@test/mock/MockPCVDepositV3.sol"; -import {MarketGovernance} from "@voltprotocol/vcon/MarketGovernance.sol"; -import {IMarketGovernance} from "@voltprotocol/vcon/IMarketGovernance.sol"; -import {ERC20HoldingPCVDeposit} from "@test/mock/ERC20HoldingPCVDeposit.sol"; -import {TestAddresses as addresses} from "@test/unit/utils/TestAddresses.sol"; - -contract UnitTestMarketGovernance is SystemUnitTest { - using SafeCast for *; - - MarketGovernance public mgov; - MockPCVSwapper public pcvSwapper; - ERC20HoldingPCVDeposit public daiHoldingDeposit; - ERC20HoldingPCVDeposit public usdcHoldingDeposit; - - uint256 public profitToVconRatioUsdc = 5; /// for each 1 wei in profit, 5 wei of vcon is received - uint256 public profitToVconRatioDai = 5; /// for each 1 wei in profit, 5 wei of vcon is received - uint256 public daiDepositAmount = 1_000_000e18; - uint256 public usdcDepositAmount = 1_000_000e6; - - uint256 public vconDepositAmount = 1_000_000e18; - address userOne = address(1000); - address userTwo = address(1001); - - /// @notice emitted when profit to vcon ratio is updated - event ProfitToVconRatioUpdated( - address indexed venue, - uint256 oldRatio, - uint256 newRatio - ); - - /// @notice emitted when the router is updated - event PCVRouterUpdated( - address indexed oldPcvRouter, - address indexed newPcvRouter - ); - - function setUp() public override { - super.setUp(); - - daiHoldingDeposit = new ERC20HoldingPCVDeposit( - coreAddress, - IERC20(address(dai)), - address(0) - ); - usdcHoldingDeposit = new ERC20HoldingPCVDeposit( - coreAddress, - IERC20(address(usdc)), - address(0) - ); - - /// can only swap from dai to usdc - pcvSwapper = new MockPCVSwapper( - MockERC20(address(dai)), - MockERC20(address(usdc)) - ); - pcvSwapper.mockSetExchangeRate(1e6); /// set dai->usdc exchange rate - - pcvRouter = new PCVRouter(coreAddress); - - mgov = new MarketGovernance(coreAddress, address(pcvRouter)); - - vm.startPrank(addresses.governorAddress); - - core.grantPCVController(address(mgov)); - core.grantLocker(address(mgov)); - core.grantLocker(address(daiHoldingDeposit)); - core.grantLocker(address(usdcHoldingDeposit)); - - address[] memory swapper = new address[](1); - swapper[0] = address(pcvSwapper); - - pcvRouter.addPCVSwappers(swapper); - - core.createRole(VoltRoles.PCV_MOVER, VoltRoles.GOVERNOR); - core.grantRole(VoltRoles.PCV_MOVER, address(mgov)); - - address[] memory venuesToAdd = new address[](2); - venuesToAdd[0] = address(daiHoldingDeposit); - venuesToAdd[1] = address(usdcHoldingDeposit); - - address[] memory oraclesToAdd = new address[](2); - oraclesToAdd[0] = address(daiConstantOracle); - oraclesToAdd[1] = address(usdcConstantOracle); - - pcvOracle.addVenues(venuesToAdd, oraclesToAdd); - - mgov.setProfitToVconRatio(address(pcvDepositDai), profitToVconRatioDai); - mgov.setProfitToVconRatio( - address(pcvDepositUsdc), - profitToVconRatioUsdc - ); - - mgov.setUnderlyingTokenHoldingDeposit( - address(dai), - address(daiHoldingDeposit) - ); - mgov.setUnderlyingTokenHoldingDeposit( - address(usdc), - address(usdcHoldingDeposit) - ); - - vm.stopPrank(); - } - - function _initializeVenues() private { - pcvDepositUsdc.setLastRecordedProfit(10_000e6); - pcvDepositDai.setLastRecordedProfit(10_000e18); - } - - function testMarketGovernanceSetup() public { - assertEq(address(mgov.core()), coreAddress); - - assertEq(mgov.profitToVconRatio(address(0)), 0); - assertEq(mgov.profitToVconRatio(address(1)), 0); - assertEq( - mgov.profitToVconRatio(address(pcvDepositDai)), - profitToVconRatioDai - ); - assertEq( - mgov.profitToVconRatio(address(pcvDepositUsdc)), - profitToVconRatioUsdc - ); - - assertTrue(core.isLocker(address(mgov))); - assertTrue(core.isPCVController(address(mgov))); - } - - /// steps: - //// define mock swapper - //// add dai -> usdc and usdc -> dai routes to mgov contract - - function testSystemOneUser() public { - _initializeVenues(); - - dai.mint(address(pcvDepositDai), daiDepositAmount); - usdc.mint(address(pcvDepositUsdc), usdcDepositAmount); - - entry.deposit(address(pcvDepositDai)); - entry.deposit(address(pcvDepositUsdc)); - - vcon.mint(address(this), vconDepositAmount); - - vcon.approve(address(mgov), vconDepositAmount); - mgov.stake(vconDepositAmount, address(pcvDepositUsdc)); - - IMarketGovernance.Rebalance[] - memory balance = new IMarketGovernance.Rebalance[](1); - balance[0] = IMarketGovernance.Rebalance({ - source: address(pcvDepositDai), - destination: address(pcvDepositUsdc), - swapper: address(pcvSwapper), - amountPcv: pcvDepositDai.balance() - }); - mgov.rebalance(balance); - - assertEq( - pcvOracle.getTotalPcv(), - pcvOracle.getVenueBalance(address(pcvDepositUsdc)) - ); - - assertEq( - mgov.venueTotalShares(address(pcvDepositUsdc)), - vconDepositAmount - ); - - assertEq(mgov.getTotalVconStaked(), vconDepositAmount); - } - - function testSystemTwoUsers() public { - uint256 totalPCV = pcvOracle.getTotalPcv(); - - testSystemOneUser(); - - totalPCV = pcvOracle.getTotalPcv(); - - vm.startPrank(userOne); - - vcon.mint(userOne, vconDepositAmount); - vcon.approve(address(mgov), vconDepositAmount); - mgov.stake(vconDepositAmount, address(pcvDepositDai)); - IMarketGovernance.Rebalance[] - memory balance = new IMarketGovernance.Rebalance[](1); - balance[0] = IMarketGovernance.Rebalance({ - source: address(pcvDepositUsdc), - destination: address(pcvDepositDai), - swapper: address(pcvSwapper), - amountPcv: pcvDepositUsdc.balance() / 2 - }); - mgov.rebalance(balance); - - vm.stopPrank(); - - assertEq(vcon.balanceOf(userOne), 0); - assertEq( - pcvOracle.getTotalPcv() / 2, - pcvOracle.getVenueBalance(address(pcvDepositUsdc)) - ); - assertEq( - pcvOracle.getTotalPcv() / 2, - pcvOracle.getVenueBalance(address(pcvDepositDai)) - ); - } - - function testSystemThreeUsersLastNoDeposit(uint120 vconAmount) public { - vm.assume(vconAmount > 1e9); - testSystemTwoUsers(); - - uint256 startingTotalSupply = mgov.getTotalVconStaked(); - uint256 startingVconStaked = mgov.venueTotalShares( - address(pcvDepositUsdc) - ); - - vm.startPrank(userTwo); - - vcon.mint(userTwo, vconAmount); - vcon.approve(address(mgov), vconAmount); - mgov.stake(vconAmount, address(pcvDepositUsdc)); - - vm.stopPrank(); - - assertEq(vcon.balanceOf(userTwo), 0); - assertEq( - mgov.venueTotalShares(address(pcvDepositUsdc)), - vconAmount + startingVconStaked - ); - assertEq( - mgov.venueUserShares(address(pcvDepositUsdc), userTwo), - vconAmount - ); - - uint256 endingTotalSupply = mgov.getTotalVconStaked(); - uint256 totalPCV = pcvOracle.getTotalPcv(); - - assertEq(endingTotalSupply, startingTotalSupply + vconAmount); - assertTrue( - mgov.getVenueDeviation( - address(pcvDepositUsdc), - totalPCV, - endingTotalSupply - ) < 0 - ); /// underweight USDC balance - assertTrue( - mgov.getVenueDeviation( - address(pcvDepositDai), - totalPCV, - endingTotalSupply - ) > 0 - ); /// overweight DAI balance - } - - function testSystemThreeUsersLastNoDepositIndividual() public { - testSystemTwoUsers(); - - uint120 vconAmount = 1e9; - address user = address(1001); - uint256 startingTotalSupply = mgov.getTotalVconStaked(); - - vm.startPrank(user); - - vcon.mint(user, vconAmount); - vcon.approve(address(mgov), vconAmount); - mgov.stake(vconAmount, address(pcvDepositUsdc)); - - vm.stopPrank(); - - uint256 endingTotalSupply = mgov.getTotalVconStaked(); - uint256 totalPCV = pcvOracle.getTotalPcv(); - - assertEq(vcon.balanceOf(user), 0); - assertEq(endingTotalSupply, startingTotalSupply + vconAmount); - assertTrue( - mgov.getVenueDeviation( - address(pcvDepositUsdc), - totalPCV, - endingTotalSupply - ) < 0 - ); /// underweight USDC balance in venues compared to staked vcon - assertTrue( - mgov.getVenueDeviation( - address(pcvDepositDai), - totalPCV, - endingTotalSupply - ) > 0 - ); /// overweight DAI balance in venues compared to staked vcon - } - - function testUserDepositsNoMove(uint120 vconAmount) public { - vm.assume(vconAmount > 1e9); - - _initializeVenues(); - - address user = address(1001); - uint256 startingTotalSupply = mgov.getTotalVconStaked(); - - vm.startPrank(user); - vcon.mint(user, vconAmount); - vcon.approve(address(mgov), vconAmount); - mgov.stake(vconAmount, address(pcvDepositUsdc)); - vm.stopPrank(); - - uint256 endingTotalSupply = mgov.getTotalVconStaked(); - uint256 totalPCV = pcvOracle.getTotalPcv(); - - assertEq(vcon.balanceOf(user), 0); - assertEq(endingTotalSupply, startingTotalSupply + vconAmount); - assertTrue( - mgov.getVenueDeviation( - address(pcvDepositUsdc), - totalPCV, - endingTotalSupply - ) < 0 - ); /// underweight USDC - assertTrue( - mgov.getVenueDeviation( - address(pcvDepositDai), - totalPCV, - endingTotalSupply - ) > 0 - ); /// overweight DAI - } - - function testUnstakingOneUserOneWei() public { - testSystemOneUser(); - - assertEq(vcon.balanceOf(address(this)), 0); - /// subtract 1 to counter the addition the protocol does - uint256 startingShareAmount = mgov.venueUserShares( - address(pcvDepositUsdc), - address(this) - ); - - mgov.unstake( - startingShareAmount, - address(pcvDepositUsdc), - address(this) - ); - - assertEq(vcon.balanceOf(address(this)), startingShareAmount); - - assertEq( - 0, - mgov.venueUserShares(address(pcvDepositUsdc), address(this)) - ); - } - - function testUnstakingOneUser() public { - testSystemOneUser(); - - assertEq(vcon.balanceOf(address(this)), 0); - uint256 shareAmount = mgov.venueUserShares( - address(pcvDepositUsdc), - address(this) - ); - uint256 vconAmount = mgov.sharesToVcon( - address(pcvDepositUsdc), - shareAmount - ); - - mgov.unstake(shareAmount, address(pcvDepositUsdc), address(this)); - - assertEq(vcon.balanceOf(address(this)), vconAmount); - - /// all funds moved to DAI Holding PCV deposit - assertEq( - pcvOracle.getTotalPcv(), - pcvOracle.getVenueBalance(address(usdcHoldingDeposit)) - ); - assertEq(0, mgov.venueTotalShares(address(pcvDepositDai))); - assertEq(0, mgov.venueTotalShares(address(pcvDepositUsdc))); - assertEq(0, pcvOracle.getVenueBalance(address(pcvDepositUsdc))); - - assertEq(0, mgov.getTotalVconStaked()); - } - - function testUnstakingTwoUsers() public { - testSystemTwoUsers(); - - assertEq(vcon.balanceOf(address(this)), 0); - - uint256 vconAmount = mgov.venueUserShares( - address(pcvDepositUsdc), - address(this) - ); - - mgov.unstake(vconAmount, address(pcvDepositUsdc), address(this)); - - assertEq(vcon.balanceOf(address(this)), vconAmount); - - { - assertEq(0, vcon.balanceOf(userOne)); - - uint256 shareAmount = mgov.venueUserShares( - address(pcvDepositDai), - userOne - ); - vm.startPrank(userOne); - mgov.unstake(shareAmount, address(pcvDepositDai), userOne); - - assertEq( - mgov.sharesToVcon(address(pcvDepositDai), shareAmount), - vcon.balanceOf(userOne) - ); - } - - /// half of funds moved to DAI PCV deposit - assertEq( - pcvOracle.getTotalPcv() / 2, - pcvOracle.getVenueBalance(address(usdcHoldingDeposit)) - ); - - /// half of funds moved to DAI PCV deposit - assertEq( - pcvOracle.getTotalPcv() / 2, - pcvOracle.getVenueBalance(address(daiHoldingDeposit)) - ); - assertEq(0, mgov.getTotalVconStaked()); - - assertEq(0, pcvOracle.getVenueBalance(address(pcvDepositUsdc))); - assertEq(0, mgov.venueTotalShares(address(pcvDepositUsdc))); - } - - function _rebalance() private { - uint256 totalPcv = pcvOracle.getTotalPcv(); - uint256 totalVconStaked = mgov.getTotalVconStaked(); - - int256 daiAmount = mgov.getVenueDeviation( - address(pcvDepositDai), - totalPcv, - totalVconStaked - ); - - if (daiAmount == 0 || totalVconStaked == 0) { - return; - } - - IMarketGovernance.Rebalance[] - memory balance = new IMarketGovernance.Rebalance[](1); - - if (daiAmount > 0) { - /// over allocated DAI deposit - balance[0] = IMarketGovernance.Rebalance({ - source: address(pcvDepositDai), - destination: address(pcvDepositUsdc), - swapper: address(pcvSwapper), - amountPcv: (daiAmount).toUint256() - 1 - }); - mgov.rebalance(balance); - } else if ( - (-daiAmount).toUint256() / 1e12 <= pcvDepositUsdc.balance() - ) { - /// under allocated DAI deposit - balance[0] = IMarketGovernance.Rebalance({ - source: address(pcvDepositUsdc), - destination: address(pcvDepositDai), - swapper: address(pcvSwapper), - amountPcv: (-daiAmount).toUint256() / 1e12 - }); - mgov.rebalance(balance); - } - } - - struct DepositInfo { - address user; - uint120 vconAmount; - uint8 venue; - } - - function testMultipleUsersStake(DepositInfo[15] memory users) public { - unchecked { - for (uint256 i = 0; i < users.length; i++) { - address user = users[i].user; - if (user == address(0)) { - /// do not allow 0 address user - users[i].user = address( - uint160(block.timestamp + block.number + i) - ); - } - if (users[i].vconAmount <= 1e18) { - /// no users will be depositing less than 1 VCON in the system - users[i].vconAmount += 1e18; - } - } - } - - uint256 totalPcv = pcvOracle.getTotalPcv(); - uint256 totalVconStaked = 0; - uint256 daiVconStaked = 0; - uint256 usdcVconStaked = 0; - - unchecked { - for (uint256 i = 0; i < users.length; i++) { - address user = users[i].user; - uint256 amount = users[i].vconAmount; - totalVconStaked += amount; - - vcon.mint(user, amount); - - vm.startPrank(user); - vcon.approve(address(mgov), amount); - - if (users[i].venue % 2 == 0) { - daiVconStaked += amount; - mgov.stake(amount, address(pcvDepositDai)); - } else { - usdcVconStaked += amount; - mgov.stake(amount, address(pcvDepositUsdc)); - } - - vm.stopPrank(); - } - } - - _rebalance(); - - IMarketGovernance.PCVDepositInfo[] memory expectedOutput = mgov - .getExpectedPCVAmounts(); - - unchecked { - for (uint256 i = 0; i < expectedOutput.length; i++) { - if (expectedOutput[i].amount >= 1e18) { - assertApproxEq( - pcvOracle - .getVenueBalance(expectedOutput[i].deposit) - .toInt256(), - expectedOutput[i].amount.toInt256(), - 0 - ); - } - } - } - - assertEq(mgov.getTotalVconStaked(), totalVconStaked); - assertTrue( - mgov.getVenueDeviation( - address(pcvDepositDai), - totalPcv, - totalVconStaked - ) < 1e18 - ); - assertTrue( - mgov.getVenueDeviation( - address(pcvDepositUsdc), - totalPcv, - totalVconStaked - ) < 1e18 - ); - } - - function testMultipleUsersUnstake(DepositInfo[15] memory users) public { - testMultipleUsersStake(users); - uint256 totalVconStaked = 0; - uint256 totalPcv = pcvOracle.getTotalPcv(); - - unchecked { - for (uint256 i = 0; i < users.length; i++) { - address user = users[i].user; - uint256 vconAmount = users[i].vconAmount; - address venue = users[i].venue % 2 == 0 - ? address(pcvDepositDai) - : address(pcvDepositUsdc); - totalVconStaked += vconAmount; - - uint256 startingUserVconBalance = vcon.balanceOf(user); - uint256 shareAmount = mgov.vconToShares(venue, vconAmount); - - vm.prank(user); - mgov.unstake(shareAmount, venue, user); - - uint256 endingUserVconBalance = vcon.balanceOf(user); - - assertEq( - endingUserVconBalance - startingUserVconBalance, - vconAmount - ); - } - } - - assertEq(mgov.getTotalVconStaked(), 0); - - /// by this point, all VCON should be unstaked so the amount of PCV in the venue should be minimal - assertTrue( - mgov.getVenueDeviation( - address(pcvDepositDai), - totalPcv, - totalVconStaked - ) < 1e18 - ); - assertTrue( - mgov.getVenueDeviation( - address(pcvDepositUsdc), - totalPcv, - totalVconStaked - ) < 1e6 - ); - - /// assert 99.99999999999999999% of PCV withdrawn - assertTrue(pcvDepositDai.balance() < 2); - assertTrue(pcvDepositUsdc.balance() < 2); - } - - /// TODO add rebalancing tests - - function testStakeAndApplyLosses( - DepositInfo[15] memory users, - uint8 shareDenominator - ) public { - vm.assume(shareDenominator > 1); /// not 0 or 1 - testMultipleUsersStake(users); - - uint256 totalVconStaked = mgov.getTotalVconStaked(); - uint128 sharePrice = mgov.venueLastRecordedVconSharePrice( - address(pcvDepositDai) - ) / shareDenominator; - - /// mark things down - vm.startPrank(addresses.governorAddress); - mgov.applyVenueLosses(address(pcvDepositDai), sharePrice); - mgov.applyVenueLosses(address(pcvDepositUsdc), sharePrice); - vm.stopPrank(); - - assertApproxEq( - (totalVconStaked / shareDenominator).toInt256(), - mgov.getTotalVconStaked().toInt256(), - 0 - ); - } - - function testSharePriceStaysConstantNoSharesWithProfit() public { - uint128 startingSharePrice = mgov.venueLastRecordedVconSharePrice( - address(pcvDepositDai) - ); - int128 startingLastRecordedProfit = mgov.venueLastRecordedProfit( - address(pcvDepositDai) - ); - - pcvDepositDai.setLastRecordedProfit(20_000e18); - mgov.accrueVcon(address(pcvDepositDai)); - - int128 endingLastRecordedProfit = mgov.venueLastRecordedProfit( - address(pcvDepositDai) - ); - uint128 endingSharePrice = mgov.venueLastRecordedVconSharePrice( - address(pcvDepositDai) - ); - - assertEq(startingSharePrice, endingSharePrice); - assertTrue(endingLastRecordedProfit > startingLastRecordedProfit); - } - - /// apply gain x across period y with z amount of users - function testSharePriceStepWise( - uint96 periodGain, - uint8 periods, - address[15] memory users - ) public { - vm.assume(periodGain > 1e18); - uint256 vconAmount = 1e18; - for (uint256 i = 0; i < periods; ) { - address user = users[i % 15] == address(0) - ? address(uint160(i + 1)) - : users[i % 15]; - vcon.mint(user, vconAmount); - - vm.startPrank(user); - vcon.approve(address(mgov), vconAmount); - mgov.stake(vconAmount, address(pcvDepositDai)); - vm.stopPrank(); - - uint256 totalVconStaked = mgov.getVenueVconStaked( - address(pcvDepositDai) - ); - uint256 totalShares = mgov.venueTotalShares(address(pcvDepositDai)); - uint256 venueStartingPrice = mgov.venueLastRecordedVconSharePrice( - address(pcvDepositDai) - ); - uint256 vconRatio = mgov.profitToVconRatio(address(pcvDepositDai)); - - uint256 vconInflation = periodGain * vconRatio; - uint256 expectedVenueSharePrice = venueStartingPrice + - (Constants.ETH_GRANULARITY * vconInflation) / - totalShares; - - pcvDepositDai.setLastRecordedProfit( - (pcvDepositDai.lastRecordedProfit() + periodGain).toInt256() - ); - - mgov.accrueVcon(address(pcvDepositDai)); - - assertApproxEq( - (expectedVenueSharePrice).toInt256(), - mgov - .venueLastRecordedVconSharePrice(address(pcvDepositDai)) - .toInt256(), - 0 - ); - assertApproxEq( - (vconInflation + totalVconStaked).toInt256(), - mgov.getVenueVconStaked(address(pcvDepositDai)).toInt256(), - 0 - ); - - unchecked { - i++; - } - /// apply gain - /// ensure getTotalVconStaked returns correct number - } - } - - /// Gain and loss scenarios - function testWithdrawWithGains() public { - uint120 vconAmount = 1000e18; - testSystemThreeUsersLastNoDeposit(vconAmount); - - pcvDepositUsdc.setLastRecordedProfit(20_000e6); - pcvDepositDai.setLastRecordedProfit(20_000e18); - - /// how much VCON is owed? - uint256 vconOwedUsdcRewards = 10_000e6 * profitToVconRatioUsdc; - uint256 vconOwedDaiRewards = 10_000e18 * profitToVconRatioDai; - - uint256 totalVconRewardsOwed = vconOwedUsdcRewards + vconOwedDaiRewards; - vcon.mint(address(mgov), totalVconRewardsOwed); - - uint256 startingVconBalance = vcon.balanceOf(address(this)); - uint256 shareAmount = mgov.venueUserShares( - address(pcvDepositUsdc), - address(this) - ); - - mgov.accrueVcon(address(pcvDepositUsdc)); - - mgov.unstake(shareAmount, address(pcvDepositUsdc), address(this)); - - uint256 endingVconBalance = vcon.balanceOf(address(this)); - - assertTrue(endingVconBalance > startingVconBalance); /// beef up these assertions to ensure share price is correct - } - - function testWithdrawWithLossesFailsDai() public { - uint120 vconAmount = 1000e18; - testSystemThreeUsersLastNoDeposit(vconAmount); - pcvDepositDai.setLastRecordedProfit(0); - - uint256 shareAmount = mgov.venueUserShares( - address(pcvDepositDai), - address(this) - ); - - vm.expectRevert("MarketGovernance: loss scenario"); - mgov.unstake(shareAmount, address(pcvDepositDai), address(this)); - } - - function testWithdrawWithLossesFailsUsdc() public { - uint120 vconAmount = 1000e18; - testSystemThreeUsersLastNoDeposit(vconAmount); - pcvDepositUsdc.setLastRecordedProfit(0); - - uint256 shareAmount = mgov.venueUserShares( - address(pcvDepositUsdc), - address(this) - ); - - vm.expectRevert("MarketGovernance: loss scenario"); - mgov.unstake(shareAmount, address(pcvDepositUsdc), address(this)); - } - - function testSetProfitToVconRatio(uint8 venueNumber, uint256 ratio) public { - address venue = venueNumber % 2 == 0 - ? address(daiHoldingDeposit) - : address(usdcHoldingDeposit); - uint256 oldProfitToVconRatio = mgov.profitToVconRatio(venue); - - vm.expectEmit(true, true, false, true, address(mgov)); - emit ProfitToVconRatioUpdated(venue, oldProfitToVconRatio, ratio); - - vm.prank(addresses.governorAddress); - mgov.setProfitToVconRatio(venue, ratio); - - assertEq(mgov.profitToVconRatio(venue), ratio); - } - - function testSetPCVRouter(address newRouter) public { - address oldPCVRouter = mgov.pcvRouter(); - - vm.expectEmit(true, true, false, true, address(mgov)); - emit PCVRouterUpdated(oldPCVRouter, newRouter); - - vm.prank(addresses.governorAddress); - mgov.setPCVRouter(newRouter); - - assertEq(mgov.pcvRouter(), newRouter); - } - - /// todo test withdrawing - /// todo test withdrawing when there are profits - /// todo test withdraw failing when there are losses - - /// todo test stake, unstake, rebalance, accrueVcon, realize gains - /// and losses with invalid venues to ensure reverts - function testStakeInvalidVenueFails() public { - vm.expectRevert("MarketGovernance: invalid destination"); - mgov.stake(0, address(0)); - } - - function testAccrueInvalidVenueFails() public { - vm.expectRevert("MarketGovernance: invalid destination"); - mgov.accrueVcon(address(0)); - } - - function testAccrueInLossVenueFails() public { - pcvDepositDai.setLastRecordedProfit(20_000e18); - mgov.accrueVcon(address(pcvDepositDai)); - - pcvDepositDai.setLastRecordedProfit(0); - - vm.expectRevert("MarketGovernance: loss scenario"); - mgov.accrueVcon(address(pcvDepositDai)); - } - - function testSetProfitToVconRatioFailsInvalidVenue() public { - vm.expectRevert("MarketGovernance: invalid venue"); - vm.prank(addresses.governorAddress); - mgov.setProfitToVconRatio(address(0), 0); - } - - function testUnstakeInvalidSourceVenueFails() public { - vm.expectRevert("MarketGovernance: invalid venue"); - mgov.unstake(0, address(0), address(0)); - } - - function testSetUnderlyingDepositFailsUnderlyingMismatch() public { - vm.expectRevert("MarketGovernance: underlying mismatch"); - vm.prank(addresses.governorAddress); - mgov.setUnderlyingTokenHoldingDeposit( - address(usdc), - address(daiHoldingDeposit) - ); - } - - function testSetUnderlyingDepositFailsInvalidVenue() public { - vm.expectRevert("MarketGovernance: invalid venue"); - vm.prank(addresses.governorAddress); - mgov.setUnderlyingTokenHoldingDeposit(address(usdc), address(0)); - } - - function testApplyLossesFailsInvalidVenue() public { - vm.expectRevert("MarketGovernance: invalid venue"); - vm.prank(addresses.governorAddress); - mgov.applyVenueLosses(address(0), 1); - } - - function testApplyLossesFailsZeroSharePrice() public { - vm.expectRevert("MarketGovernance: cannot set share price to 0"); - vm.prank(addresses.governorAddress); - mgov.applyVenueLosses(address(pcvDepositDai), 0); - } - - function testApplyLossesFailsSharePriceMarkup() public { - mgov.accrueVcon(address(pcvDepositDai)); - uint128 sharePrice = mgov.venueLastRecordedVconSharePrice( - address(pcvDepositDai) - ); - vm.expectRevert("MarketGovernance: share price not less"); - vm.prank(addresses.governorAddress); - mgov.applyVenueLosses(address(pcvDepositDai), sharePrice + 10); - } - - /// ACL tests - function testSetProfitToVconRatioFailsNonGovernor() public { - vm.expectRevert("CoreRef: Caller is not a governor"); - mgov.setProfitToVconRatio(address(0), 0); - } - - function testSetPCVRouterFailsNonGovernor() public { - vm.expectRevert("CoreRef: Caller is not a governor"); - mgov.setPCVRouter(address(0)); - } - - function testSetUnderlyingDepositFailsNonGovernor() public { - vm.expectRevert("CoreRef: Caller is not a governor"); - mgov.setUnderlyingTokenHoldingDeposit( - address(dai), - address(daiHoldingDeposit) - ); - } - - function testApplyLossesFailsNonGovernor() public { - vm.expectRevert("CoreRef: Caller is not a governor"); - mgov.applyVenueLosses(address(dai), 1); - } - - //// Pause tests - - function testPause() public { - vm.prank(addresses.governorAddress); - mgov.pause(); - - assertTrue(mgov.paused()); - } - - function testRebalanceFailsWhenPaused() public { - testPause(); - - IMarketGovernance.Rebalance[] - memory balance = new IMarketGovernance.Rebalance[](1); - balance[0] = IMarketGovernance.Rebalance({ - source: address(pcvDepositDai), - destination: address(pcvDepositUsdc), - swapper: address(pcvSwapper), - amountPcv: pcvDepositDai.balance() - }); - vm.expectRevert("Pausable: paused"); - - mgov.rebalance(balance); - } - - function testStakingFailsWhenPaused() public { - testPause(); - - vm.expectRevert("Pausable: paused"); - mgov.stake(0, address(0)); - } - - function testUnstakingFailsWhenPaused() public { - testPause(); - - vm.expectRevert("Pausable: paused"); - mgov.unstake(0, address(0), address(this)); - } - - function testAccrueFailsWhenPaused() public { - testPause(); - - vm.expectRevert("Pausable: paused"); - mgov.accrueVcon(address(0)); - } -} From 91d3c05f70626f95cec4dbd3483242b572b839ba Mon Sep 17 00:00:00 2001 From: Elliot Date: Wed, 1 Mar 2023 12:15:59 -0700 Subject: [PATCH 70/74] remove morpho deposits --- src/pcv/morpho/MorphoAavePCVDeposit.sol | 119 ----- src/pcv/morpho/MorphoCompoundPCVDeposit.sol | 118 ----- ...egrationTestMorphoCompoundPCVDeposit.t.sol | 309 ----------- ...IntegrationTestCompoundBadDebtSentinel.sol | 105 ---- .../IntegrationTestEulerPCVDeposit.sol | 92 ---- .../IntegrationTestMorphoAavePCVDeposit.sol | 97 ---- .../IntegrationTestPCVGuardian.sol | 57 -- .../IntegrationTestPCVOracle.sol | 84 --- .../IntegrationTestPCVRouter.sol | 79 --- .../IntegrationTestRateLimiters.sol | 213 -------- .../IntegrationTestRoles.sol | 209 -------- .../IntegrationTestUseNCPSMs.sol | 54 -- .../IntegrationTestProposalPSMOracle.sol | 123 ----- .../InvariantTestMorphoPCVDeposit.t.sol | 171 ------ test/mock/MockMorphoMaliciousReentrancy.sol | 59 --- test/mock/MockOracleRef.sol | 2 +- test/proposals/TestProposals.sol | 3 +- test/proposals/vips/vip16.sol | 426 +++++++-------- test/proposals/vips/vip17.sol | 2 +- .../compound/CompoundBadDebtSentinel.t.sol | 339 ------------ .../pcv/morpho/MorphoCompoundPCVDeposit.t.sol | 370 ------------- test/unit/refs/OracleRef.t.sol | 2 +- test/unit/system/System.t.sol | 490 ------------------ 23 files changed, 186 insertions(+), 3337 deletions(-) delete mode 100644 src/pcv/morpho/MorphoAavePCVDeposit.sol delete mode 100644 src/pcv/morpho/MorphoCompoundPCVDeposit.sol delete mode 100644 test/integration/IntegrationTestMorphoCompoundPCVDeposit.t.sol delete mode 100644 test/integration/post-proposal-checks/IntegrationTestCompoundBadDebtSentinel.sol delete mode 100644 test/integration/post-proposal-checks/IntegrationTestEulerPCVDeposit.sol delete mode 100644 test/integration/post-proposal-checks/IntegrationTestMorphoAavePCVDeposit.sol delete mode 100644 test/integration/post-proposal-checks/IntegrationTestPCVGuardian.sol delete mode 100644 test/integration/post-proposal-checks/IntegrationTestPCVOracle.sol delete mode 100644 test/integration/post-proposal-checks/IntegrationTestPCVRouter.sol delete mode 100644 test/integration/post-proposal-checks/IntegrationTestRateLimiters.sol delete mode 100644 test/integration/post-proposal-checks/IntegrationTestRoles.sol delete mode 100644 test/integration/post-proposal-checks/IntegrationTestUseNCPSMs.sol delete mode 100644 test/integration/proposal-checks/IntegrationTestProposalPSMOracle.sol delete mode 100644 test/invariant/InvariantTestMorphoPCVDeposit.t.sol delete mode 100644 test/mock/MockMorphoMaliciousReentrancy.sol delete mode 100644 test/unit/pcv/compound/CompoundBadDebtSentinel.t.sol delete mode 100644 test/unit/pcv/morpho/MorphoCompoundPCVDeposit.t.sol delete mode 100644 test/unit/system/System.t.sol diff --git a/src/pcv/morpho/MorphoAavePCVDeposit.sol b/src/pcv/morpho/MorphoAavePCVDeposit.sol deleted file mode 100644 index a5624a501..000000000 --- a/src/pcv/morpho/MorphoAavePCVDeposit.sol +++ /dev/null @@ -1,119 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity =0.8.13; - -import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -import {ILens} from "@voltprotocol/pcv/morpho/ILens.sol"; -import {IMorpho} from "@voltprotocol/pcv/morpho/IMorpho.sol"; -import {CoreRefV2} from "@voltprotocol/refs/CoreRefV2.sol"; -import {PCVDepositV2} from "@voltprotocol/pcv/PCVDepositV2.sol"; - -/// @notice PCV Deposit for Morpho-Aave V2. -/// Implements the PCV Deposit interface to deposit and withdraw funds in Morpho -/// Liquidity profile of Morpho for this deposit is fully liquid for USDC and DAI -/// because the incentivized rates are higher than the P2P rate. -/// Only for depositing USDC and DAI. USDT is not in scope. -/// @dev approves the Morpho Deposit to spend this PCV deposit's token, -/// and then calls supply on Morpho, which pulls the underlying token to Morpho, -/// drawing down on the approved amount to be spent, -/// and then giving this PCV Deposit credits on Morpho in exchange for the underlying -/// @dev Depositing and withdrawing in a single block will cause a very small loss -/// of funds, less than a pip. The way to not realize this loss is by depositing and -/// then withdrawing at least 1 block later. That way, interest accrues. -/// This is not a Morpho specific issue. -/// TODO confirm that Aave rounds in the protocol's favor. -/// The issue is caused by constraints inherent to solidity and the EVM. -/// There are no floating point numbers, this means there is precision loss, -/// and protocol engineers are forced to choose who to round in favor of. -/// Engineers must round in favor of the protocol to avoid deposits of 0 giving -/// the user a balance. -contract MorphoAavePCVDeposit is PCVDepositV2 { - using SafeERC20 for IERC20; - - /// ------------------------------------------ - /// ---------- Immutables/Constant ----------- - /// ------------------------------------------ - - /// @notice reference to the lens contract for morpho-aave v2 - address public immutable lens; - - /// @notice reference to the morpho-aave v2 market - address public immutable morpho; - - /// @notice aToken in aave this deposit tracks - /// used to inform morpho about the desired market to supply liquidity - address public immutable aToken; - - /// @param _core reference to the core contract - /// @param _aToken aToken this deposit references - /// @param _underlying Token denomination of this deposit - /// @param _rewardToken Reward token denomination of this deposit - /// @param _morpho reference to the morpho-aave v2 market - /// @param _lens reference to the morpho-aave v2 lens - constructor( - address _core, - address _aToken, - address _underlying, - address _rewardToken, - address _morpho, - address _lens - ) PCVDepositV2(_underlying, _rewardToken) CoreRefV2(_core) { - aToken = _aToken; - morpho = _morpho; - lens = _lens; - } - - /// ------------------------------------------ - /// ------------------ Views ----------------- - /// ------------------------------------------ - - /// @notice Returns the distribution of assets supplied by this contract through Morpho-Aave. - /// @return sum of suppliedP2P and suppliedOnPool for the given aToken - function balance() public view override returns (uint256) { - (, , uint256 totalSupplied) = ILens(lens).getCurrentSupplyBalanceInOf( - aToken, - address(this) - ); - - return totalSupplied; - } - - /// ------------------------------------------ - /// ------------- Helper Methods ------------- - /// ------------------------------------------ - - /// @notice accrue interest in the underlying morpho venue - function _accrueUnderlying() internal override { - /// accrue interest in Morpho Aave - IMorpho(morpho).updateIndexes(aToken); - } - - /// @dev withdraw from the underlying morpho market. - function _withdrawAndTransfer( - uint256 amount, - address to - ) internal override { - IMorpho(morpho).withdraw(aToken, amount); - IERC20(token).safeTransfer(to, amount); - } - - /// @dev deposit in the underlying morpho market. - function _supply(uint256 amount) internal override { - IERC20(token).approve(address(morpho), amount); - IMorpho(morpho).supply( - aToken, /// aToken to supply liquidity to - address(this), /// the address of the user you want to supply on behalf of - amount - ); - } - - /// @dev claim rewards from the underlying aave market. - /// returns amount of reward tokens claimed - function _claim() internal override returns (uint256) { - address[] memory aTokens = new address[](1); - aTokens[0] = aToken; - - return IMorpho(morpho).claimRewards(aTokens, false); /// bool set false to receive COMP - } -} diff --git a/src/pcv/morpho/MorphoCompoundPCVDeposit.sol b/src/pcv/morpho/MorphoCompoundPCVDeposit.sol deleted file mode 100644 index a7d437a45..000000000 --- a/src/pcv/morpho/MorphoCompoundPCVDeposit.sol +++ /dev/null @@ -1,118 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity =0.8.13; - -import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -import {ILens} from "@voltprotocol/pcv/morpho/ILens.sol"; -import {IMorpho} from "@voltprotocol/pcv/morpho/IMorpho.sol"; -import {CoreRefV2} from "@voltprotocol/refs/CoreRefV2.sol"; -import {PCVDepositV2} from "@voltprotocol/pcv/PCVDepositV2.sol"; - -/// @notice PCV Deposit for Morpho-Compound V2. -/// Implements the PCV Deposit interface to deposit and withdraw funds in Morpho -/// Liquidity profile of Morpho for this deposit is fully liquid for USDC and DAI -/// because the incentivized rates are higher than the P2P rate. -/// Only for depositing USDC and DAI. USDT is not in scope. -/// @dev approves the Morpho Deposit to spend this PCV deposit's token, -/// and then calls supply on Morpho, which pulls the underlying token to Morpho, -/// drawing down on the approved amount to be spent, -/// and then giving this PCV Deposit credits on Morpho in exchange for the underlying -/// @dev Depositing and withdrawing in a single block will cause a very small loss -/// of funds, less than a pip. The way to not realize this loss is by depositing and -/// then withdrawing at least 1 block later. That way, interest accrues. -/// This is not a Morpho specific issue. Compound rounds in the protocol's favor. -/// The issue is caused by constraints inherent to solidity and the EVM. -/// There are no floating point numbers, this means there is precision loss, -/// and protocol engineers are forced to choose who to round in favor of. -/// Engineers must round in favor of the protocol to avoid deposits of 0 giving -/// the user a balance. -contract MorphoCompoundPCVDeposit is PCVDepositV2 { - using SafeERC20 for IERC20; - - /// ------------------------------------------ - /// ---------- Immutables/Constant ----------- - /// ------------------------------------------ - - /// @notice reference to the lens contract for morpho-compound v2 - address public immutable lens; - - /// @notice reference to the morpho-compound v2 market - address public immutable morpho; - - /// @notice cToken in compound this deposit tracks - /// used to inform morpho about the desired market to supply liquidity - address public immutable cToken; - - /// @param _core reference to the core contract - /// @param _cToken cToken this deposit references - /// @param _underlying Token denomination of this deposit - /// @param _rewardToken Reward token denomination of this deposit - /// @param _morpho reference to the morpho-compound v2 market - /// @param _lens reference to the morpho-compound v2 lens - constructor( - address _core, - address _cToken, - address _underlying, - address _rewardToken, - address _morpho, - address _lens - ) PCVDepositV2(_underlying, _rewardToken) CoreRefV2(_core) { - cToken = _cToken; - morpho = _morpho; - lens = _lens; - } - - /// ------------------------------------------ - /// ------------------ Views ----------------- - /// ------------------------------------------ - - /// @notice Returns the distribution of assets supplied by this contract through Morpho-Compound. - /// @return sum of suppliedP2P and suppliedOnPool for the given CToken - function balance() public view override returns (uint256) { - (, , uint256 totalSupplied) = ILens(lens).getCurrentSupplyBalanceInOf( - cToken, - address(this) - ); - - return totalSupplied; - } - - /// ------------------------------------------ - /// ------------- Helper Methods ------------- - /// ------------------------------------------ - - /// @notice accrue interest in the underlying morpho venue - function _accrueUnderlying() internal override { - /// accrue interest in Morpho - IMorpho(morpho).updateP2PIndexes(cToken); - } - - /// @dev withdraw from the underlying morpho market. - function _withdrawAndTransfer( - uint256 amount, - address to - ) internal override { - IMorpho(morpho).withdraw(cToken, amount); - IERC20(token).safeTransfer(to, amount); - } - - /// @dev deposit in the underlying morpho market. - function _supply(uint256 amount) internal override { - IERC20(token).approve(address(morpho), amount); - IMorpho(morpho).supply( - cToken, /// cToken to supply liquidity to - address(this), /// the address of the user you want to supply on behalf of - amount - ); - } - - /// @dev claim rewards from the underlying Compound market. - /// returns amount of reward tokens claimed - function _claim() internal override returns (uint256) { - address[] memory cTokens = new address[](1); - cTokens[0] = cToken; - - return IMorpho(morpho).claimRewards(cTokens, false); /// bool set false to receive COMP - } -} diff --git a/test/integration/IntegrationTestMorphoCompoundPCVDeposit.t.sol b/test/integration/IntegrationTestMorphoCompoundPCVDeposit.t.sol deleted file mode 100644 index 2b201fcaa..000000000 --- a/test/integration/IntegrationTestMorphoCompoundPCVDeposit.t.sol +++ /dev/null @@ -1,309 +0,0 @@ -//SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity =0.8.13; - -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -import {Test} from "@forge-std/Test.sol"; -import {CoreV2} from "@voltprotocol/core/CoreV2.sol"; -import {getCoreV2} from "@test/unit/utils/Fixtures.sol"; -import {IPCVOracle} from "@voltprotocol/oracle/IPCVOracle.sol"; -import {PCVGuardian} from "@voltprotocol/pcv/PCVGuardian.sol"; -import {SystemEntry} from "@voltprotocol/entry/SystemEntry.sol"; -import {GenericCallMock} from "@test/mock/GenericCallMock.sol"; -import {MorphoCompoundPCVDeposit} from "@voltprotocol/pcv/morpho/MorphoCompoundPCVDeposit.sol"; -import {TestAddresses as addresses} from "@test/unit/utils/TestAddresses.sol"; -import {IGlobalReentrancyLock, GlobalReentrancyLock} from "@voltprotocol/core/GlobalReentrancyLock.sol"; - -contract IntegrationTestMorphoCompoundPCVDeposit is Test { - using SafeCast for *; - - // Constant addresses - address private constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; - address private constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; - address private constant COMP = 0xc00e94Cb662C3520282E6f5717214004A7f26888; - address private constant CDAI = 0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643; - address private constant CUSDC = 0x39AA39c021dfbaE8faC545936693aC917d5E7563; - address private constant MORPHO = - 0x8888882f8f843896699869179fB6E4f7e3B58888; - address private constant MORPHO_LENS = - 0x930f1b46e1D081Ec1524efD95752bE3eCe51EF67; - address private constant DAI_USDC_USDT_CURVE_POOL = - 0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7; - - CoreV2 private core; - SystemEntry public entry; - GlobalReentrancyLock private lock; - MorphoCompoundPCVDeposit private daiDeposit; - MorphoCompoundPCVDeposit private usdcDeposit; - - PCVGuardian private pcvGuardian; - - IERC20 private dai = IERC20(DAI); - IERC20 private usdc = IERC20(USDC); - IERC20 private comp = IERC20(COMP); - - uint256 public daiBalance; - uint256 public usdcBalance; - - uint256 targetDaiBalance = 100_000e18; - uint256 targetUsdcBalance = 100_000e6; - - uint256 public constant epochLength = 100 days; - - function setUp() public { - core = getCoreV2(); - lock = new GlobalReentrancyLock(address(core)); - daiDeposit = new MorphoCompoundPCVDeposit( - address(core), - CDAI, - DAI, - COMP, - MORPHO, - MORPHO_LENS - ); - - usdcDeposit = new MorphoCompoundPCVDeposit( - address(core), - CUSDC, - USDC, - COMP, - MORPHO, - MORPHO_LENS - ); - - entry = new SystemEntry(address(core)); - - address[] memory toWhitelist = new address[](2); - toWhitelist[0] = address(usdcDeposit); - toWhitelist[1] = address(daiDeposit); - - pcvGuardian = new PCVGuardian( - address(core), - address(this), - toWhitelist - ); - - GenericCallMock mock = new GenericCallMock(); - - vm.label(address(daiDeposit), "Morpho DAI Compound PCV Deposit"); - vm.label(address(usdcDeposit), "Morpho USDC Compound PCV Deposit"); - vm.label(address(CDAI), "CDAI"); - vm.label(address(CUSDC), "CUSDC"); - vm.label(address(usdc), "USDC"); - vm.label(address(dai), "DAI"); - vm.label(address(mock), "Mock PCV Oracle"); - vm.label(0x930f1b46e1D081Ec1524efD95752bE3eCe51EF67, "Morpho Lens"); - vm.label(0x8888882f8f843896699869179fB6E4f7e3B58888, "MORPHO_COMPOUND"); - - deal(address(dai), address(daiDeposit), targetDaiBalance); - deal(address(usdc), address(usdcDeposit), targetUsdcBalance); - - vm.startPrank(addresses.governorAddress); - - core.setGlobalReentrancyLock(IGlobalReentrancyLock(address(lock))); - - core.grantPCVGuard(address(this)); - core.grantPCVController(address(pcvGuardian)); - - core.grantLocker(address(entry)); - core.grantLocker(address(daiDeposit)); - core.grantLocker(address(usdcDeposit)); - core.grantLocker(address(pcvGuardian)); - core.setPCVOracle(IPCVOracle(address(mock))); - - vm.stopPrank(); - - /// everything is a venue - mock.setResponseToCall( - address(0), - "", - abi.encode(true), - bytes4(keccak256("isVenue(address)")) - ); - - mock.setResponseToCall( - address(0), - "", - abi.encode(uint256(0)), - bytes4(keccak256("lastRecordedPCVRaw(address)")) - ); - - entry.deposit(address(daiDeposit)); - entry.deposit(address(usdcDeposit)); - - vm.roll(block.number + 1); /// fast forward 1 block so that profit is positive - } - - function testSetup() public { - assertEq(address(daiDeposit.core()), address(core)); - assertEq(address(usdcDeposit.core()), address(core)); - assertEq(daiDeposit.morpho(), MORPHO); - assertEq(usdcDeposit.morpho(), MORPHO); - assertEq(daiDeposit.lens(), MORPHO_LENS); - assertEq(usdcDeposit.lens(), MORPHO_LENS); - - assertEq(daiDeposit.token(), address(dai)); - assertEq(usdcDeposit.token(), address(usdc)); - - assertEq(address(daiDeposit.cToken()), address(CDAI)); - assertEq(address(usdcDeposit.cToken()), address(CUSDC)); - - assertEq(address(daiDeposit.token()), address(DAI)); - assertEq(address(usdcDeposit.token()), address(USDC)); - - // assertEq(daiDeposit.lastRecordedBalance(), targetDaiBalance); - // assertEq(usdcDeposit.lastRecordedBalance(), targetUsdcBalance); - - assertApproxEq( - daiDeposit.balance().toInt256(), - targetDaiBalance.toInt256(), - 0 - ); - assertApproxEq( - usdcDeposit.balance().toInt256(), - targetUsdcBalance.toInt256(), - 0 - ); - } - - function testWithdraw() public { - pcvGuardian.withdrawToSafeAddress( - address(usdcDeposit), - usdcDeposit.balance() - ); - pcvGuardian.withdrawToSafeAddress( - address(daiDeposit), - daiDeposit.balance() - ); - - assertApproxEq( - dai.balanceOf(address(this)).toInt256(), - targetDaiBalance.toInt256(), - 0 - ); - assertApproxEq( - usdc.balanceOf(address(this)).toInt256(), - targetUsdcBalance.toInt256(), - 0 - ); - } - - function testHarvest() public { - /// fast forward block number amount - vm.roll(block.number + epochLength / 12); - - uint256 startingCompBalance = comp.balanceOf(address(usdcDeposit)) + - comp.balanceOf(address(daiDeposit)); - - entry.harvest(address(usdcDeposit)); - entry.harvest(address(daiDeposit)); - - uint256 endingCompBalance = comp.balanceOf(address(usdcDeposit)) + - comp.balanceOf(address(daiDeposit)); - - uint256 compDelta = endingCompBalance - startingCompBalance; - - assertTrue(compDelta != 0); - } - - /// 2**80 / 1e18 = ~1.2m which is above target dai balance - function testWithdrawDaiFuzz(uint80 amount) public { - /// 1 fails in some underlying contract, and this isn't a scenario we are going to realistically have - /// as 1e9 wei of dai would always cost more in gas than the dai is worth - vm.assume(amount >= 1e9); - vm.assume(amount <= targetDaiBalance); - - pcvGuardian.withdrawToSafeAddress(address(daiDeposit), amount); - - assertEq(dai.balanceOf(address(this)), amount); - - assertApproxEq( - daiDeposit.balance().toInt256(), - (targetDaiBalance - amount).toInt256(), - 0 - ); - } - - function testWithdrawUsdcFuzz(uint40 amount) public { - vm.assume(amount != 0); - vm.assume(amount <= targetUsdcBalance); - - pcvGuardian.withdrawToSafeAddress(address(usdcDeposit), amount); - - assertEq(usdc.balanceOf(address(this)), amount); - - assertApproxEq( - usdcDeposit.balance().toInt256(), - (targetUsdcBalance - amount).toInt256(), - 0 - ); - } - - function testWithdrawAll() public { - pcvGuardian.withdrawAllToSafeAddress(address(usdcDeposit)); - pcvGuardian.withdrawAllToSafeAddress(address(daiDeposit)); - - assertApproxEq( - dai.balanceOf(address(this)).toInt256(), - targetDaiBalance.toInt256(), - 0 - ); - - assertApproxEq( - usdc.balanceOf(address(this)).toInt256(), - targetUsdcBalance.toInt256(), - 0 - ); - } - - function testDepositNoFundsSucceeds() public { - entry.deposit(address(usdcDeposit)); - entry.deposit(address(daiDeposit)); - } - - function testAccrueNoFundsSucceeds() public { - entry.accrue(address(usdcDeposit)); - entry.accrue(address(daiDeposit)); - } - - function testDepositWhenPausedFails() public { - vm.prank(addresses.governorAddress); - usdcDeposit.pause(); - - vm.expectRevert("Pausable: paused"); - entry.deposit(address(usdcDeposit)); - - vm.prank(addresses.governorAddress); - daiDeposit.pause(); - - vm.expectRevert("Pausable: paused"); - entry.deposit(address(daiDeposit)); - } - - function testWithdrawNonPCVControllerFails() public { - vm.startPrank(addresses.governorAddress); - core.grantLocker(addresses.governorAddress); - lock.lock(1); - vm.stopPrank(); - - vm.expectRevert("CoreRef: Caller is not a PCV controller"); - usdcDeposit.withdraw(address(this), targetUsdcBalance); - - vm.expectRevert("CoreRef: Caller is not a PCV controller"); - daiDeposit.withdraw(address(this), targetDaiBalance); - } - - function testWithdrawAllNonPCVControllerFails() public { - vm.startPrank(addresses.governorAddress); - core.grantLocker(addresses.governorAddress); - lock.lock(1); - vm.stopPrank(); - - vm.expectRevert("CoreRef: Caller is not a PCV controller"); - usdcDeposit.withdrawAll(address(this)); - - vm.expectRevert("CoreRef: Caller is not a PCV controller"); - daiDeposit.withdrawAll(address(this)); - } -} diff --git a/test/integration/post-proposal-checks/IntegrationTestCompoundBadDebtSentinel.sol b/test/integration/post-proposal-checks/IntegrationTestCompoundBadDebtSentinel.sol deleted file mode 100644 index cf39ad95b..000000000 --- a/test/integration/post-proposal-checks/IntegrationTestCompoundBadDebtSentinel.sol +++ /dev/null @@ -1,105 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.13; - -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -import {PCVOracle} from "@voltprotocol/oracle/PCVOracle.sol"; -import {PCVGuardian} from "@voltprotocol/pcv/PCVGuardian.sol"; -import {IPCVDepositV2} from "@voltprotocol/pcv/IPCVDepositV2.sol"; -import {PostProposalCheck} from "@test/integration/post-proposal-checks/PostProposalCheck.sol"; -import {CompoundBadDebtSentinel} from "@voltprotocol/pcv/compound/CompoundBadDebtSentinel.sol"; - -contract IntegrationTestCompoundBadDebtSentinel is PostProposalCheck { - using SafeCast for *; - - function testBadDebtOverThresholdAllowsSentinelWithdraw() public { - vm.roll(block.number + 1); /// compound time advance to accrue interest - vm.warp(block.timestamp + 15); /// euler time advance to accrue interest - - CompoundBadDebtSentinel badDebtSentinel = CompoundBadDebtSentinel( - addresses.mainnet("COMPOUND_BAD_DEBT_SENTINEL") - ); - PCVGuardian pcvGuardian = PCVGuardian( - addresses.mainnet("PCV_GUARDIAN") - ); - PCVOracle pcvOracle = PCVOracle(addresses.mainnet("PCV_ORACLE")); - IPCVDepositV2 daiDeposit = IPCVDepositV2( - addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_DAI") - ); - IPCVDepositV2 usdcDeposit = IPCVDepositV2( - addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_USDC") - ); - - address yearn = 0x342491C093A640c7c2347c4FFA7D8b9cBC84D1EB; - - /// zero cDAI and cUSDC balances to create bad debt - deal(addresses.mainnet("CUSDC"), yearn, 0); - deal(addresses.mainnet("CDAI"), yearn, 0); - - address[] memory user = new address[](1); - user[0] = yearn; - - assertApproxEq( - int256(daiDeposit.balance()), - pcvOracle.lastRecordedPCV(address(daiDeposit)).toInt256(), - 0 - ); - assertApproxEq( - int256(usdcDeposit.balance()) * 1e12, - pcvOracle.lastRecordedPCV(address(usdcDeposit)).toInt256(), - 0 - ); - - assertTrue(badDebtSentinel.getTotalBadDebt(user) > 10_000_000e18); - - badDebtSentinel.rescueAllFromCompound(user); - - // sanity checks - assertTrue(daiDeposit.balance() < 10e18); - assertTrue(usdcDeposit.balance() < 10e6); - - address safeAddress = pcvGuardian.safeAddress(); - assertTrue(safeAddress != address(0)); - - assertTrue( - IERC20(addresses.mainnet("DAI")).balanceOf(safeAddress) > - 1_000_000 * 1e18 - ); - // assertTrue( - // IERC20(addresses.mainnet("USDC")).balanceOf(safeAddress) > - // 10_000 * 1e6 - // ); - } - - function testNoBadDebtBlocksSentinelWithdraw() public { - vm.roll(block.number + 1); /// compound time advance to accrue interest - vm.warp(block.timestamp + 15); /// euler time advance to accrue interest - - CompoundBadDebtSentinel badDebtSentinel = CompoundBadDebtSentinel( - addresses.mainnet("COMPOUND_BAD_DEBT_SENTINEL") - ); - IPCVDepositV2 daiDeposit = IPCVDepositV2( - addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_DAI") - ); - IPCVDepositV2 usdcDeposit = IPCVDepositV2( - addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_USDC") - ); - - address yearn = 0x342491C093A640c7c2347c4FFA7D8b9cBC84D1EB; - - address[] memory users = new address[](2); - users[0] = yearn; /// yearn is less than morpho, place it first to order list - users[1] = addresses.mainnet("MORPHO_COMPOUND"); - - assertEq(badDebtSentinel.getTotalBadDebt(users), 0); - - uint256 daiBalance = daiDeposit.balance(); - uint256 usdcBalance = usdcDeposit.balance(); - - badDebtSentinel.rescueAllFromCompound(users); - - assertEq(daiDeposit.balance(), daiBalance); - assertEq(usdcDeposit.balance(), usdcBalance); - } -} diff --git a/test/integration/post-proposal-checks/IntegrationTestEulerPCVDeposit.sol b/test/integration/post-proposal-checks/IntegrationTestEulerPCVDeposit.sol deleted file mode 100644 index 372e0ada2..000000000 --- a/test/integration/post-proposal-checks/IntegrationTestEulerPCVDeposit.sol +++ /dev/null @@ -1,92 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.13; - -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -import {SystemEntry} from "@voltprotocol/entry/SystemEntry.sol"; -import {IPCVDepositV2} from "@voltprotocol/pcv/IPCVDepositV2.sol"; -import {EulerPCVDeposit} from "@voltprotocol/pcv/euler/EulerPCVDeposit.sol"; -import {PostProposalCheck} from "@test/integration/post-proposal-checks/PostProposalCheck.sol"; - -contract IntegrationTestEulerPCVDeposit is PostProposalCheck { - using SafeCast for *; - - uint256 depositAmount = 1_000_000; - - function testCanDepositEuler() public { - SystemEntry entry = SystemEntry(addresses.mainnet("SYSTEM_ENTRY")); - EulerPCVDeposit daiDeposit = EulerPCVDeposit( - addresses.mainnet("PCV_DEPOSIT_EULER_DAI") - ); - EulerPCVDeposit usdcDeposit = EulerPCVDeposit( - addresses.mainnet("PCV_DEPOSIT_EULER_USDC") - ); - - deal( - addresses.mainnet("DAI"), - address(daiDeposit), - depositAmount * 1e18 - ); - deal( - addresses.mainnet("USDC"), - address(usdcDeposit), - depositAmount * 1e6 - ); - - entry.deposit(address(daiDeposit)); - entry.deposit(address(usdcDeposit)); - - assertApproxEq( - daiDeposit.balance().toInt256(), - (depositAmount * 1e18).toInt256(), - 0 - ); - assertApproxEq( - usdcDeposit.balance().toInt256(), - (depositAmount * 1e6).toInt256(), - 0 - ); - } - - function testHarvestEuler() public { - testCanDepositEuler(); - - SystemEntry entry = SystemEntry(addresses.mainnet("SYSTEM_ENTRY")); - EulerPCVDeposit daiDeposit = EulerPCVDeposit( - addresses.mainnet("PCV_DEPOSIT_EULER_DAI") - ); - EulerPCVDeposit usdcDeposit = EulerPCVDeposit( - addresses.mainnet("PCV_DEPOSIT_EULER_USDC") - ); - - entry.harvest(address(daiDeposit)); - entry.harvest(address(usdcDeposit)); - } - - function testAccrueEuler() public { - testCanDepositEuler(); - - SystemEntry entry = SystemEntry(addresses.mainnet("SYSTEM_ENTRY")); - EulerPCVDeposit daiDeposit = EulerPCVDeposit( - addresses.mainnet("PCV_DEPOSIT_EULER_DAI") - ); - EulerPCVDeposit usdcDeposit = EulerPCVDeposit( - addresses.mainnet("PCV_DEPOSIT_EULER_USDC") - ); - - uint256 daiBalance = entry.accrue(address(daiDeposit)); - uint256 usdcBalance = entry.accrue(address(usdcDeposit)); - - assertApproxEq( - daiBalance.toInt256(), - (depositAmount * 1e18).toInt256(), - 0 - ); - assertApproxEq( - usdcBalance.toInt256(), - (depositAmount * 1e6).toInt256(), - 0 - ); - } -} diff --git a/test/integration/post-proposal-checks/IntegrationTestMorphoAavePCVDeposit.sol b/test/integration/post-proposal-checks/IntegrationTestMorphoAavePCVDeposit.sol deleted file mode 100644 index 52bce09c4..000000000 --- a/test/integration/post-proposal-checks/IntegrationTestMorphoAavePCVDeposit.sol +++ /dev/null @@ -1,97 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.13; - -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -import {console} from "@forge-std/console.sol"; -import {SystemEntry} from "@voltprotocol/entry/SystemEntry.sol"; -import {IPCVDepositV2} from "@voltprotocol/pcv/IPCVDepositV2.sol"; -import {PostProposalCheck} from "@test/integration/post-proposal-checks/PostProposalCheck.sol"; -import {MorphoAavePCVDeposit} from "@voltprotocol/pcv/morpho/MorphoAavePCVDeposit.sol"; - -contract IntegrationTestMorphoAavePCVDeposit is PostProposalCheck { - using SafeCast for *; - - uint256 depositAmount = 1_000_000; - - function testCanDepositAave() public { - SystemEntry entry = SystemEntry(addresses.mainnet("SYSTEM_ENTRY")); - MorphoAavePCVDeposit daiDeposit = MorphoAavePCVDeposit( - addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_DAI") - ); - MorphoAavePCVDeposit usdcDeposit = MorphoAavePCVDeposit( - addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_USDC") - ); - - deal( - addresses.mainnet("DAI"), - address(daiDeposit), - depositAmount * 1e18 - ); - deal( - addresses.mainnet("USDC"), - address(usdcDeposit), - depositAmount * 1e6 - ); - - entry.deposit(address(daiDeposit)); - entry.deposit(address(usdcDeposit)); - - assertApproxEq( - daiDeposit.balance().toInt256(), - (depositAmount * 1e18).toInt256(), - 0 - ); - assertApproxEq( - usdcDeposit.balance().toInt256(), - (depositAmount * 1e6).toInt256(), - 0 - ); - } - - /// liquidity mining is over for aave, so harvesting fails - function testHarvestFailsAave() public { - testCanDepositAave(); - - SystemEntry entry = SystemEntry(addresses.mainnet("SYSTEM_ENTRY")); - MorphoAavePCVDeposit daiDeposit = MorphoAavePCVDeposit( - addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_DAI") - ); - MorphoAavePCVDeposit usdcDeposit = MorphoAavePCVDeposit( - addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_USDC") - ); - - vm.expectRevert(); - entry.harvest(address(daiDeposit)); - - vm.expectRevert(); - entry.harvest(address(usdcDeposit)); - } - - function testAccrueAave() public { - testCanDepositAave(); - - SystemEntry entry = SystemEntry(addresses.mainnet("SYSTEM_ENTRY")); - MorphoAavePCVDeposit daiDeposit = MorphoAavePCVDeposit( - addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_DAI") - ); - MorphoAavePCVDeposit usdcDeposit = MorphoAavePCVDeposit( - addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_USDC") - ); - - uint256 daiBalance = entry.accrue(address(daiDeposit)); - uint256 usdcBalance = entry.accrue(address(usdcDeposit)); - - assertApproxEq( - daiBalance.toInt256(), - (depositAmount * 1e18).toInt256(), - 0 - ); - assertApproxEq( - usdcBalance.toInt256(), - (depositAmount * 1e6).toInt256(), - 0 - ); - } -} diff --git a/test/integration/post-proposal-checks/IntegrationTestPCVGuardian.sol b/test/integration/post-proposal-checks/IntegrationTestPCVGuardian.sol deleted file mode 100644 index dae726226..000000000 --- a/test/integration/post-proposal-checks/IntegrationTestPCVGuardian.sol +++ /dev/null @@ -1,57 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.13; - -import {PostProposalCheck} from "@test/integration/post-proposal-checks/PostProposalCheck.sol"; - -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {PCVGuardian} from "@voltprotocol/pcv/PCVGuardian.sol"; -import {IPCVDepositV2} from "@voltprotocol/pcv/IPCVDepositV2.sol"; - -contract IntegrationTestPCVGuardian is PostProposalCheck { - function testWithdrawAllToSafeAddress() public { - address[6] memory addressesToClean = [ - addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_DAI"), - addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_USDC"), - addresses.mainnet("PCV_DEPOSIT_EULER_DAI"), - addresses.mainnet("PCV_DEPOSIT_EULER_USDC"), - addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_DAI"), - addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_DAI") - ]; - - PCVGuardian pcvGuardian = PCVGuardian( - addresses.mainnet("PCV_GUARDIAN") - ); - - vm.roll(block.number + 1); /// compound time advance to accrue interest - vm.warp(block.timestamp + 15); /// euler time advance to accrue interest - - vm.startPrank(addresses.mainnet("GOVERNOR")); - - for (uint256 i = 0; i < addressesToClean.length; i++) { - if (IPCVDepositV2(addressesToClean[i]).balance() != 0) { - pcvGuardian.withdrawAllToSafeAddress(addressesToClean[i]); - } - - // Check only dust left after withdrawals - assertLt(IPCVDepositV2(addressesToClean[i]).balance(), 1e6); - } - - vm.stopPrank(); - - // sanity checks - address safeAddress = pcvGuardian.safeAddress(); - assertTrue(safeAddress != address(0)); - require( - IERC20(addresses.mainnet("DAI")).balanceOf(safeAddress) > - 1_000_000 * 1e18, - "Low DAI" - ); // >1M DAI - require( - IERC20(addresses.mainnet("USDC")).balanceOf(safeAddress) > - 10_000 * 1e6, - "Low USDC" - ); // >10k USDC - - vm.revertTo(postProposalsSnapshot); // undo withdrawals - } -} diff --git a/test/integration/post-proposal-checks/IntegrationTestPCVOracle.sol b/test/integration/post-proposal-checks/IntegrationTestPCVOracle.sol deleted file mode 100644 index 637235423..000000000 --- a/test/integration/post-proposal-checks/IntegrationTestPCVOracle.sol +++ /dev/null @@ -1,84 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.13; - -import {PostProposalCheck} from "@test/integration/post-proposal-checks/PostProposalCheck.sol"; - -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -import {CoreV2} from "@voltprotocol/core/CoreV2.sol"; -import {VoltV2} from "@voltprotocol/volt/VoltV2.sol"; -import {PCVOracle} from "@voltprotocol/oracle/PCVOracle.sol"; -import {IPCVOracle} from "@voltprotocol/oracle/IPCVOracle.sol"; -import {PCVGuardian} from "@voltprotocol/pcv/PCVGuardian.sol"; -import {GenericCallMock} from "@test/mock/GenericCallMock.sol"; -import {NonCustodialPSM} from "@voltprotocol/peg/NonCustodialPSM.sol"; - -contract IntegrationTestPCVOracle is PostProposalCheck { - CoreV2 private core; - IERC20 private dai; - VoltV2 private volt; - address private grlm; - PCVOracle private pcvOracle; - NonCustodialPSM private daipsm; - PCVGuardian private pcvGuardian; - address private morphoDaiPCVDeposit; - - function setUp() public override { - super.setUp(); - - core = CoreV2(addresses.mainnet("CORE")); - dai = IERC20(addresses.mainnet("DAI")); - volt = VoltV2(addresses.mainnet("VOLT")); - grlm = addresses.mainnet("GLOBAL_RATE_LIMITED_MINTER"); - morphoDaiPCVDeposit = addresses.mainnet( - "PCV_DEPOSIT_MORPHO_COMPOUND_DAI" - ); - daipsm = NonCustodialPSM(addresses.mainnet("PSM_NONCUSTODIAL_DAI")); - pcvOracle = PCVOracle(addresses.mainnet("PCV_ORACLE")); - pcvGuardian = PCVGuardian(addresses.mainnet("PCV_GUARDIAN")); - } - - // check that we can read the pcv and it does not revert - function testReadPcv() public { - uint256 totalPcv = pcvOracle.getTotalPcv(); - assertTrue(totalPcv > 0, "Zero PCV"); - } - - // check that we can unset the PCVOracle in Core - // and that it doesn't break PCV movements (only disables accounting). - function testUnsetPcvOracle() public { - address multisig = addresses.mainnet("GOVERNOR"); - - GenericCallMock mock = new GenericCallMock(); - - mock.setResponseToCall( - address(0), - "", - abi.encode(true), - bytes4(keccak256("isVenue(address)")) - ); - - mock.setResponseToCall( - address(0), - "", - abi.encode(uint256(0)), - bytes4(keccak256("lastRecordedPCVRaw(address)")) - ); - - vm.prank(multisig); - core.setPCVOracle(IPCVOracle(address(mock))); - - vm.prank(multisig); - pcvGuardian.withdrawToSafeAddress(morphoDaiPCVDeposit, 100e18); - - // No revert & PCV moved - assertEq(dai.balanceOf(pcvGuardian.safeAddress()), 100e18); - - // User redeems - vm.prank(grlm); - volt.mint(address(this), 100e18); - volt.approve(address(daipsm), 100e18); - daipsm.redeem(address(this), 100e18, 104e18); - assertGt(dai.balanceOf(address(this)), 104e18); - } -} diff --git a/test/integration/post-proposal-checks/IntegrationTestPCVRouter.sol b/test/integration/post-proposal-checks/IntegrationTestPCVRouter.sol deleted file mode 100644 index f2598b1bf..000000000 --- a/test/integration/post-proposal-checks/IntegrationTestPCVRouter.sol +++ /dev/null @@ -1,79 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.13; - -import {PostProposalCheck} from "@test/integration/post-proposal-checks/PostProposalCheck.sol"; - -import {PCVRouter} from "@voltprotocol/pcv/PCVRouter.sol"; -import {IPCVDepositV2} from "@voltprotocol/pcv/IPCVDepositV2.sol"; - -contract IntegrationTestPCVRouter is PostProposalCheck { - uint256 private constant AMOUNT = 5_000; - - // Validate that pcv router can be used to move funds - // Move 5000 DAI from MorphoCompoundDAI PCVDeposit to - // 5000 USDC in MorphoCompoundUSDC PCVDeposit. - function testPcvRouterWithSwap() public { - PCVRouter pcvRouter = PCVRouter(addresses.mainnet("PCV_ROUTER")); - IPCVDepositV2 daiDeposit = IPCVDepositV2( - addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_DAI") - ); - IPCVDepositV2 usdcDeposit = IPCVDepositV2( - addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_USDC") - ); - address pcvMover = addresses.mainnet("GOVERNOR"); // an address with PCV_MOVER role - - // read balances before - uint256 depositDaiBalanceBefore = daiDeposit.balance(); - uint256 depositUsdcBalanceBefore = usdcDeposit.balance(); - - // Swap DAI to USDC - vm.startPrank(pcvMover); - pcvRouter.movePCVUnchecked( - address(daiDeposit), // source - address(usdcDeposit), // destination - addresses.mainnet("PCV_SWAPPER_MAKER"), // swapper - AMOUNT * 1e18, // amount - addresses.mainnet("DAI"), // sourceAsset - addresses.mainnet("USDC") // destinationAsset - ); - vm.stopPrank(); - - uint256 depositDaiBalanceAfter = daiDeposit.balance(); - uint256 depositUsdcBalanceAfter = usdcDeposit.balance(); - - // tolerate 0.5% err because morpho withdrawals are not exact - assertGt( - depositDaiBalanceBefore - depositDaiBalanceAfter, - (995 * AMOUNT * 1e18) / 1000 - ); - assertGt( - depositUsdcBalanceAfter - depositUsdcBalanceBefore, - ((995 * AMOUNT * 1e18) / 1e12) / 1000 - ); - - // Swap USDC to DAI (half of previous amount) - vm.startPrank(pcvMover); // has PCV_MOVER role - pcvRouter.movePCVUnchecked( - address(usdcDeposit), // source - address(daiDeposit), // destination - addresses.mainnet("PCV_SWAPPER_MAKER"), // swapper - (AMOUNT * 1e18) / 2e12, // amount - addresses.mainnet("USDC"), // sourceAsset - addresses.mainnet("DAI") // destinationAsset - ); - vm.stopPrank(); - - uint256 depositDaiBalanceFinal = daiDeposit.balance(); - uint256 depositUsdcBalanceFinal = usdcDeposit.balance(); - - // tolerate 0.5% err because morpho withdrawals are not exact - assertGt( - depositDaiBalanceFinal - depositDaiBalanceAfter, - (995 * AMOUNT * 1e18) / 2000 - ); - assertGt( - depositUsdcBalanceAfter - depositUsdcBalanceFinal, - ((995 * AMOUNT * 1e18) / 1e12) / 2000 - ); - } -} diff --git a/test/integration/post-proposal-checks/IntegrationTestRateLimiters.sol b/test/integration/post-proposal-checks/IntegrationTestRateLimiters.sol deleted file mode 100644 index f9bf9ca1e..000000000 --- a/test/integration/post-proposal-checks/IntegrationTestRateLimiters.sol +++ /dev/null @@ -1,213 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.13; - -import {PostProposalCheck} from "@test/integration/post-proposal-checks/PostProposalCheck.sol"; - -import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; - -import {VoltV2} from "@voltprotocol/volt/VoltV2.sol"; -import {CoreV2} from "@voltprotocol/core/CoreV2.sol"; -import {PCVOracle} from "@voltprotocol/oracle/PCVOracle.sol"; -import {PCVDeposit} from "@voltprotocol/pcv/PCVDeposit.sol"; -import {SystemEntry} from "@voltprotocol/entry/SystemEntry.sol"; -import {NonCustodialPSM} from "@voltprotocol/peg/NonCustodialPSM.sol"; -import {GlobalRateLimitedMinter} from "@voltprotocol/rate-limits/GlobalRateLimitedMinter.sol"; - -contract IntegrationTestRateLimiters is PostProposalCheck { - using SafeCast for *; - - address public constant user = 0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7; - uint256 public snapshotAfterMints; - - IERC20 private dai; - IERC20 private usdc; - CoreV2 private core; - VoltV2 private volt; - PCVOracle private pcvOracle; - SystemEntry private systemEntry; - NonCustodialPSM private daincpsm; - NonCustodialPSM private usdcncpsm; - GlobalRateLimitedMinter private grlm; - address private morphoDaiPCVDeposit; - address private morphoUsdcPCVDeposit; - - function setUp() public override { - super.setUp(); - - core = CoreV2(addresses.mainnet("CORE")); - volt = VoltV2(addresses.mainnet("VOLT")); - systemEntry = SystemEntry(addresses.mainnet("SYSTEM_ENTRY")); - daincpsm = NonCustodialPSM(addresses.mainnet("PSM_NONCUSTODIAL_DAI")); - usdcncpsm = NonCustodialPSM(addresses.mainnet("PSM_NONCUSTODIAL_USDC")); - dai = IERC20(addresses.mainnet("DAI")); - usdc = IERC20(addresses.mainnet("USDC")); - grlm = GlobalRateLimitedMinter( - addresses.mainnet("GLOBAL_RATE_LIMITED_MINTER") - ); - pcvOracle = PCVOracle(addresses.mainnet("PCV_ORACLE")); - morphoUsdcPCVDeposit = addresses.mainnet( - "PCV_DEPOSIT_MORPHO_COMPOUND_USDC" - ); - morphoDaiPCVDeposit = addresses.mainnet( - "PCV_DEPOSIT_MORPHO_COMPOUND_DAI" - ); - } - - /* - Flow of the first user that mints VOLT in the new system. - Performs checks on the global rate limits, and accounting - in the new system's PCV Oracle. - */ - function testUserPSMMint() public { - { - uint256 initialBuffer = grlm.buffer(); - - // number of moved funds for tests - uint256 amount = initialBuffer / 2; - - // user performs the first mint with DAI - vm.startPrank(user); - dai.approve(address(daincpsm), amount); - daincpsm.mint(user, amount, 0); - vm.stopPrank(); - - // buffer has been used - uint256 voltReceived1 = volt.balanceOf(user); - assertEq(grlm.buffer(), initialBuffer - voltReceived1); - - // user performs the second mint wit USDC - vm.startPrank(user); - usdc.approve(address(usdcncpsm), amount / 1e12); - usdcncpsm.mint(user, amount / 1e12, 0); - vm.stopPrank(); - uint256 voltReceived2 = volt.balanceOf(user) - voltReceived1; - - // buffer has been used - assertEq( - grlm.buffer(), - initialBuffer - voltReceived1 - voltReceived2 - ); - } - - snapshotAfterMints = vm.snapshot(); - - vm.prank(address(core)); - grlm.setRateLimitPerSecond(5.787e18); - - // buffer replenishes over time - vm.warp(block.timestamp + 3 days); - - // above limit rate reverts - uint256 largeAmount = grlm.bufferCap() * 2; - vm.startPrank(user); - dai.approve(address(daincpsm), largeAmount); - vm.expectRevert("RateLimited: buffer cap underflow"); - daincpsm.mint(user, largeAmount, 0); - vm.stopPrank(); - } - - function testRedeemsDaiPsm(uint88 voltAmount) public { - vm.assume(voltAmount >= 1e18); - - testUserPSMMint(); - - vm.revertTo(snapshotAfterMints); - vm.assume(voltAmount <= volt.balanceOf(user)); - - uint256 daiAmountOut = daincpsm.getRedeemAmountOut(voltAmount); - deal(address(dai), morphoDaiPCVDeposit, daiAmountOut); - systemEntry.deposit(morphoDaiPCVDeposit); - - vm.startPrank(user); - - uint256 startingBuffer = grlm.buffer(); - uint256 startingDaiBalance = dai.balanceOf(user); - - volt.approve(address(daincpsm), voltAmount); - daincpsm.redeem(user, voltAmount, daiAmountOut); - - uint256 endingBuffer = grlm.buffer(); - uint256 endingDaiBalance = dai.balanceOf(user); - - assertEq(endingDaiBalance - startingDaiBalance, daiAmountOut); - assertEq(endingBuffer - startingBuffer, voltAmount); - - vm.stopPrank(); - } - - function testRedeemsDaiNcPsm(uint80 voltRedeemAmount) public { - vm.assume(voltRedeemAmount >= 1e18); - vm.assume(voltRedeemAmount <= 400_000e18); - testUserPSMMint(); - - vm.revertTo(snapshotAfterMints); - - uint256 daiAmountOut = daincpsm.getRedeemAmountOut(voltRedeemAmount); - deal(address(dai), morphoDaiPCVDeposit, daiAmountOut * 2); - systemEntry.deposit(morphoDaiPCVDeposit); - - uint256 startingBuffer = grlm.buffer(); - uint256 startingDaiBalance = dai.balanceOf(user); - - vm.startPrank(user); - volt.approve(address(daincpsm), voltRedeemAmount); - daincpsm.redeem(user, voltRedeemAmount, daiAmountOut); - vm.stopPrank(); - - uint256 endingBuffer = grlm.buffer(); - uint256 endingDaiBalance = dai.balanceOf(user); - - assertEq(endingBuffer - startingBuffer, voltRedeemAmount); /// grlm buffer replenished - assertEq(endingDaiBalance - startingDaiBalance, daiAmountOut); - } - - function testRedeemsUsdcNcPsm(uint80 voltRedeemAmount) public { - vm.assume(voltRedeemAmount >= 1e18); - vm.assume(voltRedeemAmount <= 400_000e18); - testUserPSMMint(); - - vm.revertTo(snapshotAfterMints); - - uint256 usdcAmountOut = usdcncpsm.getRedeemAmountOut(voltRedeemAmount); - deal(address(usdc), morphoUsdcPCVDeposit, usdcAmountOut * 2); - systemEntry.deposit(morphoUsdcPCVDeposit); - - vm.startPrank(user); - - uint256 startingBuffer = grlm.buffer(); - uint256 startingUsdcBalance = usdc.balanceOf(user); - - volt.approve(address(usdcncpsm), voltRedeemAmount); - usdcncpsm.redeem(user, voltRedeemAmount, usdcAmountOut); - - uint256 endingBuffer = grlm.buffer(); - uint256 endingUsdcBalance = usdc.balanceOf(user); - - assertEq(endingBuffer - startingBuffer, voltRedeemAmount); /// buffer replenished - assertEq(endingUsdcBalance - startingUsdcBalance, usdcAmountOut); - - vm.stopPrank(); - } - - function testRedeemsUsdcPsm(uint80 voltRedeemAmount) public { - vm.assume(voltRedeemAmount >= 1e18); - vm.assume(voltRedeemAmount <= 475_000e18); - testUserPSMMint(); - - vm.revertTo(snapshotAfterMints); - - uint256 usdcAmountOut = usdcncpsm.getRedeemAmountOut(voltRedeemAmount); - deal(address(usdc), morphoUsdcPCVDeposit, usdcAmountOut); - systemEntry.deposit(morphoUsdcPCVDeposit); - - uint256 startingBuffer = grlm.buffer(); - uint256 startingUsdcBalance = usdc.balanceOf(user); - - vm.startPrank(user); - volt.approve(address(usdcncpsm), voltRedeemAmount); - usdcncpsm.redeem(user, voltRedeemAmount, usdcAmountOut); - vm.stopPrank(); - } -} diff --git a/test/integration/post-proposal-checks/IntegrationTestRoles.sol b/test/integration/post-proposal-checks/IntegrationTestRoles.sol deleted file mode 100644 index cdbeea4ed..000000000 --- a/test/integration/post-proposal-checks/IntegrationTestRoles.sol +++ /dev/null @@ -1,209 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.13; - -import {PostProposalCheck} from "@test/integration/post-proposal-checks/PostProposalCheck.sol"; - -import {CoreV2} from "@voltprotocol/core/CoreV2.sol"; -import {VoltRoles} from "@voltprotocol/core/VoltRoles.sol"; - -contract IntegrationTestRoles is PostProposalCheck { - function testMainnetRoles() public { - CoreV2 core = CoreV2(addresses.mainnet("CORE")); - - // GOVERNOR - assertEq(core.getRoleAdmin(VoltRoles.GOVERNOR), VoltRoles.GOVERNOR); - assertEq(core.getRoleMemberCount(VoltRoles.GOVERNOR), 3); - assertEq( - core.getRoleMember(VoltRoles.GOVERNOR, 0), - addresses.mainnet("CORE") - ); - assertEq( - core.getRoleMember(VoltRoles.GOVERNOR, 1), - addresses.mainnet("GOVERNOR") - ); - assertEq( - core.getRoleMember(VoltRoles.GOVERNOR, 2), - addresses.mainnet("TIMELOCK_CONTROLLER") - ); - - // PCV_CONTROLLER - assertEq( - core.getRoleAdmin(VoltRoles.PCV_CONTROLLER), - VoltRoles.GOVERNOR - ); - assertEq(core.getRoleMemberCount(VoltRoles.PCV_CONTROLLER), 5); - - assertEq( - core.getRoleMember(VoltRoles.PCV_CONTROLLER, 0), - addresses.mainnet("PCV_GUARDIAN") - ); - assertEq( - core.getRoleMember(VoltRoles.PCV_CONTROLLER, 1), - addresses.mainnet("PCV_ROUTER") - ); - assertEq( - core.getRoleMember(VoltRoles.PCV_CONTROLLER, 2), - addresses.mainnet("GOVERNOR") - ); - assertEq( - core.getRoleMember(VoltRoles.PCV_CONTROLLER, 3), - addresses.mainnet("PSM_NONCUSTODIAL_DAI") - ); - assertEq( - core.getRoleMember(VoltRoles.PCV_CONTROLLER, 4), - addresses.mainnet("PSM_NONCUSTODIAL_USDC") - ); - - // PCV_MOVER - assertEq(core.getRoleAdmin(VoltRoles.PCV_MOVER), VoltRoles.GOVERNOR); - assertEq(core.getRoleMemberCount(VoltRoles.PCV_MOVER), 1); - assertEq( - core.getRoleMember(VoltRoles.PCV_MOVER, 0), - addresses.mainnet("GOVERNOR") - ); - - // PCV_DEPOSIT_ROLE - assertEq(core.getRoleAdmin(VoltRoles.PCV_DEPOSIT), VoltRoles.GOVERNOR); - assertEq(core.getRoleMemberCount(VoltRoles.PCV_DEPOSIT), 6); - assertEq( - core.getRoleMember(VoltRoles.PCV_DEPOSIT, 0), - addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_DAI") - ); - assertEq( - core.getRoleMember(VoltRoles.PCV_DEPOSIT, 1), - addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_USDC") - ); - assertEq( - core.getRoleMember(VoltRoles.PCV_DEPOSIT, 2), - addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_DAI") - ); - assertEq( - core.getRoleMember(VoltRoles.PCV_DEPOSIT, 3), - addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_USDC") - ); - assertEq( - core.getRoleMember(VoltRoles.PCV_DEPOSIT, 4), - addresses.mainnet("PCV_DEPOSIT_EULER_DAI") - ); - assertEq( - core.getRoleMember(VoltRoles.PCV_DEPOSIT, 5), - addresses.mainnet("PCV_DEPOSIT_EULER_USDC") - ); - - // PCV_GUARD - assertEq(core.getRoleAdmin(VoltRoles.PCV_GUARD), VoltRoles.GOVERNOR); - assertEq(core.getRoleMemberCount(VoltRoles.PCV_GUARD), 3); - assertEq( - core.getRoleMember(VoltRoles.PCV_GUARD, 0), - addresses.mainnet("EOA_1") - ); - assertEq( - core.getRoleMember(VoltRoles.PCV_GUARD, 1), - addresses.mainnet("EOA_2") - ); - assertEq( - core.getRoleMember(VoltRoles.PCV_GUARD, 2), - addresses.mainnet("EOA_4") - ); - - // GUARDIAN - assertEq(core.getRoleAdmin(VoltRoles.GUARDIAN), VoltRoles.GOVERNOR); - assertEq(core.getRoleMemberCount(VoltRoles.GUARDIAN), 3); - assertEq( - core.getRoleMember(VoltRoles.GUARDIAN, 0), - addresses.mainnet("PCV_GUARDIAN") - ); - assertEq( - core.getRoleMember(VoltRoles.GUARDIAN, 1), - addresses.mainnet("GOVERNOR") // team multisig - ); - assertEq( - core.getRoleMember(VoltRoles.GUARDIAN, 2), - addresses.mainnet("COMPOUND_BAD_DEBT_SENTINEL") - ); - - // PSM_MINTER_ROLE - assertEq( - core.getRoleAdmin(VoltRoles.PSM_MINTER), - VoltRoles.GOVERNOR - ); - assertEq( - core.getRoleMemberCount( - VoltRoles.PSM_MINTER - ), - 2 - ); - assertEq( - core.getRoleMember(VoltRoles.PSM_MINTER, 0), - addresses.mainnet("PSM_NONCUSTODIAL_DAI") - ); - assertEq( - core.getRoleMember(VoltRoles.PSM_MINTER, 1), - addresses.mainnet("PSM_NONCUSTODIAL_USDC") - ); - - // LOCKER_ROLE - assertEq(core.getRoleAdmin(VoltRoles.LOCKER), VoltRoles.GOVERNOR); - assertEq(core.getRoleMemberCount(VoltRoles.LOCKER), 13); - assertEq( - core.getRoleMember(VoltRoles.LOCKER, 0), - addresses.mainnet("SYSTEM_ENTRY") - ); - assertEq( - core.getRoleMember(VoltRoles.LOCKER, 1), - addresses.mainnet("PCV_ORACLE") - ); - assertEq( - core.getRoleMember(VoltRoles.LOCKER, 2), - addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_DAI") - ); - assertEq( - core.getRoleMember(VoltRoles.LOCKER, 3), - addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_USDC") - ); - assertEq( - core.getRoleMember(VoltRoles.LOCKER, 4), - addresses.mainnet("GLOBAL_RATE_LIMITED_MINTER") - ); - assertEq( - core.getRoleMember(VoltRoles.LOCKER, 5), - addresses.mainnet("PCV_ROUTER") - ); - assertEq( - core.getRoleMember(VoltRoles.LOCKER, 6), - addresses.mainnet("PCV_GUARDIAN") - ); - assertEq( - core.getRoleMember(VoltRoles.LOCKER, 7), - addresses.mainnet("PSM_NONCUSTODIAL_DAI") - ); - assertEq( - core.getRoleMember(VoltRoles.LOCKER, 8), - addresses.mainnet("PSM_NONCUSTODIAL_USDC") - ); - assertEq( - core.getRoleMember(VoltRoles.LOCKER, 9), - addresses.mainnet("PCV_DEPOSIT_EULER_DAI") - ); - assertEq( - core.getRoleMember(VoltRoles.LOCKER, 10), - addresses.mainnet("PCV_DEPOSIT_EULER_USDC") - ); - assertEq( - core.getRoleMember(VoltRoles.LOCKER, 11), - addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_DAI") - ); - assertEq( - core.getRoleMember(VoltRoles.LOCKER, 12), - addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_USDC") - ); - - // MINTER - assertEq(core.getRoleAdmin(VoltRoles.MINTER), VoltRoles.GOVERNOR); - assertEq(core.getRoleMemberCount(VoltRoles.MINTER), 1); - assertEq( - core.getRoleMember(VoltRoles.MINTER, 0), - addresses.mainnet("GLOBAL_RATE_LIMITED_MINTER") - ); - } -} diff --git a/test/integration/post-proposal-checks/IntegrationTestUseNCPSMs.sol b/test/integration/post-proposal-checks/IntegrationTestUseNCPSMs.sol deleted file mode 100644 index 792c6e74a..000000000 --- a/test/integration/post-proposal-checks/IntegrationTestUseNCPSMs.sol +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.13; - -import {PostProposalCheck} from "@test/integration/post-proposal-checks/PostProposalCheck.sol"; - -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {IVolt} from "@voltprotocol/volt/IVolt.sol"; -import {NonCustodialPSM} from "@voltprotocol/peg/NonCustodialPSM.sol"; - -// Tests that non-custodial redeem on all PSMS do not revert. -// Assumes VOLT > 1$. -contract IntegrationTestUseNCPSMs is PostProposalCheck { - uint256 private constant AMOUNT = 5_000; - - // Redeem on non-custodial USDC PSM - function testMainnetUSDCNCRedeem() public { - NonCustodialPSM psm = NonCustodialPSM( - addresses.mainnet("PSM_NONCUSTODIAL_USDC") - ); - IERC20 token = IERC20(addresses.mainnet("USDC")); - IVolt volt = IVolt(addresses.mainnet("VOLT")); - - // (MOCK) mint VOLT for the user - vm.prank(addresses.mainnet("GLOBAL_RATE_LIMITED_MINTER")); - volt.mint(address(this), AMOUNT * 1e18); - - // do non-custodial redeem - volt.approve(address(psm), AMOUNT * 1e18); - psm.redeem(address(this), AMOUNT * 1e18, 0); - - // check received tokens - assertTrue(token.balanceOf(address(this)) > AMOUNT * 1e6); - } - - // Redeem on non-custodial DAI PSM - function testMainnetDAINCRedeem() public { - NonCustodialPSM psm = NonCustodialPSM( - addresses.mainnet("PSM_NONCUSTODIAL_DAI") - ); - IERC20 token = IERC20(addresses.mainnet("DAI")); - IVolt volt = IVolt(addresses.mainnet("VOLT")); - - // (MOCK) mint VOLT for the user - vm.prank(addresses.mainnet("GLOBAL_RATE_LIMITED_MINTER")); - volt.mint(address(this), AMOUNT * 1e18); - - // do non-custodial redeem - volt.approve(address(psm), AMOUNT * 1e18); - psm.redeem(address(this), AMOUNT * 1e18, 0); - - // check received tokens - assertTrue(token.balanceOf(address(this)) > AMOUNT * 1e18); - } -} diff --git a/test/integration/proposal-checks/IntegrationTestProposalPSMOracle.sol b/test/integration/proposal-checks/IntegrationTestProposalPSMOracle.sol deleted file mode 100644 index 84c016e6c..000000000 --- a/test/integration/proposal-checks/IntegrationTestProposalPSMOracle.sol +++ /dev/null @@ -1,123 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.13; - -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -import {Test} from "@forge-std/Test.sol"; -import {Addresses} from "@test/proposals/Addresses.sol"; -import {IOracleRef} from "@voltprotocol/v1/IOracleRef.sol"; -import {IOracleRefV2} from "@voltprotocol/refs/IOracleRefV2.sol"; -import {TestProposals} from "@test/proposals/TestProposals.sol"; -import {IPegStabilityModule} from "@voltprotocol/peg/IPegStabilityModule.sol"; - -contract IntegrationTestProposalPSMOracle is Test { - function setUp() public {} - - function testPsmOracle() public { - Addresses addresses = new Addresses(); - TestProposals proposals = new TestProposals(); - proposals.setUp(); - proposals.setDebug(false); - - // If one of the proposals skip PSM verification, - // do not perform verification. This is useful when interface - // change on the PSMs or if the oracle logic is changed. - uint256 nProposals = proposals.nProposals(); - bool doTest = true; - for (uint256 i = 0; i < nProposals; i++) { - if (proposals.proposals(i).SKIP_PSM_ORACLE_TEST()) { - doTest = false; - } - } - if (!doTest) return; - - // Read oracles pre-proposals - uint256 oraclePriceBeforeDaiPSM = IOracleRef( - addresses.mainnet("VOLT_DAI_PSM") - ).readOracle().value; - uint256 oraclePricesBeforeUsdcPSM = IOracleRef( - addresses.mainnet("VOLT_USDC_PSM") - ).readOracle().value; - - // Run all pending proposals - proposals.testProposals(); - addresses = proposals.addresses(); - - // Read oracles post-proposals - uint256 oraclePricesAfterDaiPSM = IOracleRefV2( - addresses.mainnet("PSM_DAI") - ).readOracle(); - uint256 oraclePricesAfterUsdcPSM = IOracleRefV2( - addresses.mainnet("PSM_USDC") - ).readOracle(); - - // Check oracle values are the same - assertEq( - oraclePriceBeforeDaiPSM, - oraclePricesAfterDaiPSM, - "PSM Oracle value changed (DAI old->DAI new)" - ); - assertEq( - oraclePriceBeforeDaiPSM / 1e12, - oraclePricesAfterUsdcPSM, - "PSM Oracle value changed (DAI old->USDC new)" - ); - assertEq( - oraclePricesBeforeUsdcPSM, - oraclePricesAfterDaiPSM, - "PSM Oracle value changed (USDC old->DAI new)" - ); - assertEq( - oraclePricesBeforeUsdcPSM / 1e12, - oraclePricesAfterUsdcPSM, - "PSM Oracle value changed (USDC old->USDC new)" - ); - } - - function testPSMSameMint() public { - // Init - Addresses addresses = new Addresses(); - IPegStabilityModule psm = IPegStabilityModule( - addresses.mainnet("PSM_USDC") - ); - IERC20 volt = IERC20(addresses.mainnet("VOLT")); - IERC20 token = IERC20(addresses.mainnet("USDC")); - uint256 amountTokens = 100e6; - - // Read pre-proposal VOLT minted for a known amount of USDC - deal(address(token), address(this), amountTokens); - token.approve(address(psm), amountTokens); - psm.mint(address(this), amountTokens, 0); - uint256 receivedVoltPreProposal = volt.balanceOf(address(this)); - volt.transfer(address(psm), receivedVoltPreProposal); - - // Run all pending proposals - TestProposals proposals = new TestProposals(); - proposals.setUp(); - proposals.setDebug(false); // no prints - proposals.testProposals(); - addresses = proposals.addresses(); // get post-proposal addresses - - // Use post-proposal contracts if they have been migrated - psm = IPegStabilityModule(addresses.mainnet("PSM_NONCUSTODIAL_USDC")); - volt = IERC20(addresses.mainnet("VOLT")); - token = IERC20(addresses.mainnet("USDC")); - - // Read post-proposal VOLT minted for a known amount of USDC - deal(address(token), address(this), amountTokens); - token.approve(address(psm), amountTokens); - psm.mint(address(this), amountTokens, 0); - uint256 receivedVoltPostProposal = volt.balanceOf(address(this)); - volt.transfer(address(psm), receivedVoltPostProposal); - - // Check amounts of VOLT minted for the same amount of USDC in - // are the same before & after proposals executions - // Tolerate 0.05% difference because the proposals might fast-forward - // in time and that will make the oracle prices progress. - uint256 toleratedDiff = (5 * receivedVoltPreProposal) / 10_000; - assertGt( - receivedVoltPreProposal, - receivedVoltPostProposal - toleratedDiff - ); - } -} diff --git a/test/invariant/InvariantTestMorphoPCVDeposit.t.sol b/test/invariant/InvariantTestMorphoPCVDeposit.t.sol deleted file mode 100644 index b50ad8a9d..000000000 --- a/test/invariant/InvariantTestMorphoPCVDeposit.t.sol +++ /dev/null @@ -1,171 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity =0.8.13; - -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -import {Test} from "@forge-std/Test.sol"; -import {CoreV2} from "@voltprotocol/core/CoreV2.sol"; -import {MockERC20} from "@test/mock/MockERC20.sol"; -import {getCoreV2} from "@test/unit/utils/Fixtures.sol"; -import {MockMorpho} from "@test/mock/MockMorpho.sol"; -import {IPCVOracle} from "@voltprotocol/oracle/IPCVOracle.sol"; -import {PCVGuardian} from "@voltprotocol/pcv/PCVGuardian.sol"; -import {SystemEntry} from "@voltprotocol/entry/SystemEntry.sol"; -import {MockPCVOracle} from "@test/mock/MockPCVOracle.sol"; -import {InvariantTest} from "@test/invariant/InvariantTest.sol"; -import {MorphoCompoundPCVDeposit} from "@voltprotocol/pcv/morpho/MorphoCompoundPCVDeposit.sol"; -import {TestAddresses as addresses} from "@test/unit/utils/TestAddresses.sol"; -import {IGlobalReentrancyLock, GlobalReentrancyLock} from "@voltprotocol/core/GlobalReentrancyLock.sol"; - -/// note all variables have to be public and not immutable otherwise foundry -/// will not run invariant tests - -/// @dev Modified from Solmate ERC20 Invariant Test (https://github.com/transmissions11/solmate/blob/main/src/test/ERC20.t.sol) -contract InvariantTestMorphoCompoundPCVDeposit is Test, InvariantTest { - using SafeCast for *; - - /// TODO add invariant test for profit tracking - - CoreV2 public core; - MockERC20 public token; - MockMorpho public morpho; - SystemEntry public entry; - PCVGuardian public pcvGuardian; - MockPCVOracle public pcvOracle; - IGlobalReentrancyLock private lock; - MorphoCompoundPCVDepositTest public morphoTest; - MorphoCompoundPCVDeposit public morphoDeposit; - - function setUp() public { - core = getCoreV2(); - token = new MockERC20(); - pcvOracle = new MockPCVOracle(); - morpho = new MockMorpho(IERC20(address(token))); - morphoDeposit = new MorphoCompoundPCVDeposit( - address(core), - address(morpho), - address(token), - address(0), /// no need for reward token - address(morpho), - address(morpho) - ); - lock = IGlobalReentrancyLock( - address(new GlobalReentrancyLock(address(core))) - ); - - address[] memory toWhitelist = new address[](1); - toWhitelist[0] = address(morphoDeposit); - - pcvGuardian = new PCVGuardian( - address(core), - address(this), - toWhitelist - ); - - entry = new SystemEntry(address(core)); - morphoTest = new MorphoCompoundPCVDepositTest( - morphoDeposit, - token, - morpho, - entry, - pcvGuardian - ); - - vm.startPrank(addresses.governorAddress); - - core.setPCVOracle(IPCVOracle(address(pcvOracle))); - - core.grantPCVGuard(address(morphoTest)); - core.grantPCVController(address(pcvGuardian)); - - core.grantLocker(address(entry)); - core.grantLocker(address(pcvGuardian)); - core.grantLocker(address(morphoDeposit)); - core.setGlobalReentrancyLock(lock); - - vm.stopPrank(); - - addTargetContract(address(morphoTest)); - } - - function invariantLastRecordedBalance() public { - assertEq(morphoDeposit.balance(), morphoTest.totalDeposited()); - } - - function invariantPcvOracle() public { - assertEq(morphoDeposit.balance(), morphoTest.totalDeposited()); - } - - function invariantBalanceOf() public { - assertEq( - morphoDeposit.balance(), - morpho.balances(address(morphoDeposit)) - ); - } -} - -contract MorphoCompoundPCVDepositTest is Test { - uint256 public totalDeposited; - - MockERC20 public token; - MockMorpho public morpho; - SystemEntry public entry; - PCVGuardian public pcvGuardian; - MorphoCompoundPCVDeposit public morphoDeposit; - - constructor( - MorphoCompoundPCVDeposit _morphoDeposit, - MockERC20 _token, - MockMorpho _morpho, - SystemEntry _entry, - PCVGuardian _pcvGuardian - ) { - morphoDeposit = _morphoDeposit; - token = _token; - morpho = _morpho; - entry = _entry; - pcvGuardian = _pcvGuardian; - } - - function increaseBalance(uint256 amount) public { - token.mint(address(morphoDeposit), amount); - entry.deposit(address(morphoDeposit)); - - unchecked { - /// unchecked because token or MockMorpho will revert - /// from an integer overflow - totalDeposited += amount; - } - } - - function decreaseBalance(uint256 amount) public { - if (amount > totalDeposited) return; - - pcvGuardian.withdrawToSafeAddress(address(morphoDeposit), amount); - unchecked { - /// unchecked because amount is always less than or equal - /// to totalDeposited - totalDeposited -= amount; - } - } - - function withdrawEntireBalance() public { - pcvGuardian.withdrawAllToSafeAddress(address(morphoDeposit)); - totalDeposited = 0; - } - - function increaseBalanceViaInterest(uint256 interestAmount) public { - token.mint(address(morpho), interestAmount); - morpho.setBalance( - address(morphoDeposit), - totalDeposited + interestAmount - ); - entry.accrue(address(morphoDeposit)); /// accrue interest so morpho and pcv deposit are synced - unchecked { - /// unchecked because token or MockMorpho will revert - /// from an integer overflow - totalDeposited += interestAmount; - } - } -} diff --git a/test/mock/MockMorphoMaliciousReentrancy.sol b/test/mock/MockMorphoMaliciousReentrancy.sol deleted file mode 100644 index 8fb8bd15c..000000000 --- a/test/mock/MockMorphoMaliciousReentrancy.sol +++ /dev/null @@ -1,59 +0,0 @@ -pragma solidity 0.8.13; - -import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; - -import {MorphoCompoundPCVDeposit} from "@voltprotocol/pcv/morpho/MorphoCompoundPCVDeposit.sol"; - -contract MockMorphoMaliciousReentrancy { - IERC20 public immutable token; - mapping(address => uint256) public balances; - MorphoCompoundPCVDeposit public morphoCompoundPCVDeposit; - - constructor(IERC20 _token) { - token = _token; - } - - function underlying() external view returns (address) { - return address(token); - } - - function setMorphoCompoundPCVDeposit(address deposit) external { - morphoCompoundPCVDeposit = MorphoCompoundPCVDeposit(deposit); - } - - function withdraw(address, uint256) external { - morphoCompoundPCVDeposit.accrue(); - } - - function supply(address, address, uint256) external { - morphoCompoundPCVDeposit.deposit(); - } - - function setBalance(address to, uint256 amount) external { - balances[to] = amount; - } - - function claimRewards( - address cToken, - bool swapForMorpho - ) external returns (uint256) {} - - function updateP2PIndexes(address) external { - morphoCompoundPCVDeposit.accrue(); - } - - function getCurrentSupplyBalanceInOf( - address, - address _user - ) - external - view - returns ( - uint256 balanceOnPool, - uint256 balanceInP2P, - uint256 totalBalance - ) - { - return (0, 0, balances[_user]); - } -} diff --git a/test/mock/MockOracleRef.sol b/test/mock/MockOracleRef.sol index 29264cd83..9a55ddf53 100644 --- a/test/mock/MockOracleRef.sol +++ b/test/mock/MockOracleRef.sol @@ -1,4 +1,4 @@ -/// // SPDX-License-Identifier: GPL-3.0-or-later +/// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.13; import {OracleRef} from "@voltprotocol/v1/OracleRef.sol"; diff --git a/test/proposals/TestProposals.sol b/test/proposals/TestProposals.sol index 72fa85862..a3a4e3570 100644 --- a/test/proposals/TestProposals.sol +++ b/test/proposals/TestProposals.sol @@ -8,7 +8,6 @@ import {Proposal} from "@test/proposals/proposalTypes/Proposal.sol"; import {vip15} from "@test/proposals/vips/vip15.sol"; import {vip16} from "@test/proposals/vips/vip16.sol"; -import {vip17} from "@test/proposals/vips/vip17.sol"; /* How to use: @@ -44,7 +43,7 @@ contract TestProposals is Test { proposals.push(Proposal(address(new vip15()))); proposals.push(Proposal(address(new vip16()))); - proposals.push(Proposal(address(new vip17()))); + // proposals.push(Proposal(address(new vip17()))); nProposals = proposals.length; } diff --git a/test/proposals/vips/vip16.sol b/test/proposals/vips/vip16.sol index 86fd131cb..79acbaa2b 100644 --- a/test/proposals/vips/vip16.sol +++ b/test/proposals/vips/vip16.sol @@ -25,8 +25,6 @@ import {NonCustodialPSM} from "@voltprotocol/peg/NonCustodialPSM.sol"; import {MakerPCVSwapper} from "@voltprotocol/pcv/maker/MakerPCVSwapper.sol"; import {VoltSystemOracle} from "@voltprotocol/oracle/VoltSystemOracle.sol"; import {EulerPCVDeposit} from "@voltprotocol/pcv/euler/EulerPCVDeposit.sol"; -import {MorphoCompoundPCVDeposit} from "@voltprotocol/pcv/morpho/MorphoCompoundPCVDeposit.sol"; -import {MorphoAavePCVDeposit} from "@voltprotocol/pcv/morpho/MorphoAavePCVDeposit.sol"; import {IPegStabilityModule} from "@voltprotocol/peg/IPegStabilityModule.sol"; import {ConstantPriceOracle} from "@voltprotocol/oracle/ConstantPriceOracle.sol"; import {CompoundBadDebtSentinel} from "@voltprotocol/pcv/compound/CompoundBadDebtSentinel.sol"; @@ -129,66 +127,6 @@ contract vip16 is Proposal { ); addresses.addMainnet("GLOBAL_RATE_LIMITED_MINTER", address(grlm)); } - - /// PCV Deposits - { - /// Morpho Compound Deposits - MorphoCompoundPCVDeposit morphoCompoundDaiPCVDeposit = new MorphoCompoundPCVDeposit( - addresses.mainnet("CORE"), - addresses.mainnet("CDAI"), - addresses.mainnet("DAI"), - addresses.mainnet("COMP"), - addresses.mainnet("MORPHO_COMPOUND"), - addresses.mainnet("MORPHO_LENS_COMPOUND") - ); - - MorphoCompoundPCVDeposit morphoCompoundUsdcPCVDeposit = new MorphoCompoundPCVDeposit( - addresses.mainnet("CORE"), - addresses.mainnet("CUSDC"), - addresses.mainnet("USDC"), - addresses.mainnet("COMP"), - addresses.mainnet("MORPHO_COMPOUND"), - addresses.mainnet("MORPHO_LENS_COMPOUND") - ); - - addresses.addMainnet( - "PCV_DEPOSIT_MORPHO_COMPOUND_DAI", - address(morphoCompoundDaiPCVDeposit) - ); - addresses.addMainnet( - "PCV_DEPOSIT_MORPHO_COMPOUND_USDC", - address(morphoCompoundUsdcPCVDeposit) - ); - } - { - /// Morpho Aave Deposits - MorphoAavePCVDeposit morphoAaveDaiPCVDeposit = new MorphoAavePCVDeposit( - addresses.mainnet("CORE"), - addresses.mainnet("ADAI"), - addresses.mainnet("DAI"), - address(0), - addresses.mainnet("MORPHO_AAVE"), - addresses.mainnet("MORPHO_LENS_AAVE") - ); - - MorphoAavePCVDeposit morphoAaveUsdcPCVDeposit = new MorphoAavePCVDeposit( - addresses.mainnet("CORE"), - addresses.mainnet("AUSDC"), - addresses.mainnet("USDC"), - address(0), - addresses.mainnet("MORPHO_AAVE"), - addresses.mainnet("MORPHO_LENS_AAVE") - ); - - addresses.addMainnet( - "PCV_DEPOSIT_MORPHO_AAVE_DAI", - address(morphoAaveDaiPCVDeposit) - ); - addresses.addMainnet( - "PCV_DEPOSIT_MORPHO_AAVE_USDC", - address(morphoAaveUsdcPCVDeposit) - ); - } { /// Euler Deposits EulerPCVDeposit eulerDaiPCVDeposit = new EulerPCVDeposit( @@ -228,40 +166,40 @@ contract vip16 is Proposal { /// Peg Stability { - NonCustodialPSM usdcNonCustodialPsm = new NonCustodialPSM( - addresses.mainnet("CORE"), - addresses.mainnet("VOLT_SYSTEM_ORACLE"), - address(0), - -12, - false, - IERC20(addresses.mainnet("USDC")), - VOLT_FLOOR_PRICE_USDC, - VOLT_CEILING_PRICE_USDC, - IPCVDepositV2( - addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_USDC") - ) - ); - NonCustodialPSM daiNonCustodialPsm = new NonCustodialPSM( - addresses.mainnet("CORE"), - addresses.mainnet("VOLT_SYSTEM_ORACLE"), - address(0), - 0, - false, - IERC20(addresses.mainnet("DAI")), - VOLT_FLOOR_PRICE_DAI, - VOLT_CEILING_PRICE_DAI, - IPCVDepositV2( - addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_DAI") - ) - ); - addresses.addMainnet( - "PSM_NONCUSTODIAL_USDC", - address(usdcNonCustodialPsm) - ); - addresses.addMainnet( - "PSM_NONCUSTODIAL_DAI", - address(daiNonCustodialPsm) - ); + // NonCustodialPSM usdcNonCustodialPsm = new NonCustodialPSM( + // addresses.mainnet("CORE"), + // addresses.mainnet("VOLT_SYSTEM_ORACLE"), + // address(0), + // -12, + // false, + // IERC20(addresses.mainnet("USDC")), + // VOLT_FLOOR_PRICE_USDC, + // VOLT_CEILING_PRICE_USDC, + // IPCVDepositV2( + // addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_USDC") + // ) + // ); + // NonCustodialPSM daiNonCustodialPsm = new NonCustodialPSM( + // addresses.mainnet("CORE"), + // addresses.mainnet("VOLT_SYSTEM_ORACLE"), + // address(0), + // 0, + // false, + // IERC20(addresses.mainnet("DAI")), + // VOLT_FLOOR_PRICE_DAI, + // VOLT_CEILING_PRICE_DAI, + // IPCVDepositV2( + // addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_DAI") + // ) + // ); + // addresses.addMainnet( + // "PSM_NONCUSTODIAL_USDC", + // address(usdcNonCustodialPsm) + // ); + // addresses.addMainnet( + // "PSM_NONCUSTODIAL_DAI", + // address(daiNonCustodialPsm) + // ); } /// PCV Movement @@ -274,25 +212,25 @@ contract vip16 is Proposal { addresses.mainnet("CORE") ); - address[] memory pcvGuardianSafeAddresses = new address[](6); - pcvGuardianSafeAddresses[0] = addresses.mainnet( - "PCV_DEPOSIT_MORPHO_COMPOUND_DAI" - ); - pcvGuardianSafeAddresses[1] = addresses.mainnet( - "PCV_DEPOSIT_MORPHO_COMPOUND_USDC" - ); - pcvGuardianSafeAddresses[2] = addresses.mainnet( - "PCV_DEPOSIT_EULER_DAI" - ); - pcvGuardianSafeAddresses[3] = addresses.mainnet( - "PCV_DEPOSIT_EULER_USDC" - ); - pcvGuardianSafeAddresses[4] = addresses.mainnet( - "PCV_DEPOSIT_MORPHO_AAVE_DAI" - ); - pcvGuardianSafeAddresses[5] = addresses.mainnet( - "PCV_DEPOSIT_MORPHO_AAVE_USDC" - ); + address[] memory pcvGuardianSafeAddresses = new address[](0); + // pcvGuardianSafeAddresses[0] = addresses.mainnet( + // "PCV_DEPOSIT_MORPHO_COMPOUND_DAI" + // ); + // pcvGuardianSafeAddresses[1] = addresses.mainnet( + // "PCV_DEPOSIT_MORPHO_COMPOUND_USDC" + // ); + // pcvGuardianSafeAddresses[2] = addresses.mainnet( + // "PCV_DEPOSIT_EULER_DAI" + // ); + // pcvGuardianSafeAddresses[3] = addresses.mainnet( + // "PCV_DEPOSIT_EULER_USDC" + // ); + // pcvGuardianSafeAddresses[4] = addresses.mainnet( + // "PCV_DEPOSIT_MORPHO_AAVE_DAI" + // ); + // pcvGuardianSafeAddresses[5] = addresses.mainnet( + // "PCV_DEPOSIT_MORPHO_AAVE_USDC" + // ); PCVGuardian pcvGuardian = new PCVGuardian( addresses.mainnet("CORE"), @@ -373,39 +311,39 @@ contract vip16 is Proposal { core.grantPCVController(addresses.mainnet("PCV_GUARDIAN")); core.grantPCVController(addresses.mainnet("PCV_ROUTER")); core.grantPCVController(addresses.mainnet("GOVERNOR")); /// team multisig - core.grantPCVController(addresses.mainnet("PSM_NONCUSTODIAL_DAI")); - core.grantPCVController(addresses.mainnet("PSM_NONCUSTODIAL_USDC")); + // core.grantPCVController(addresses.mainnet("PSM_NONCUSTODIAL_DAI")); + // core.grantPCVController(addresses.mainnet("PSM_NONCUSTODIAL_USDC")); core.createRole(VoltRoles.PCV_MOVER, VoltRoles.GOVERNOR); core.grantRole(VoltRoles.PCV_MOVER, addresses.mainnet("GOVERNOR")); /// team multisig - core.createRole(VoltRoles.PSM_MINTER, VoltRoles.GOVERNOR); - core.grantRole( - VoltRoles.PSM_MINTER, - addresses.mainnet("PSM_NONCUSTODIAL_DAI") - ); - core.grantRole( - VoltRoles.PSM_MINTER, - addresses.mainnet("PSM_NONCUSTODIAL_USDC") - ); + // core.createRole(VoltRoles.PSM_MINTER, VoltRoles.GOVERNOR); + // core.grantRole( + // VoltRoles.PSM_MINTER, + // addresses.mainnet("PSM_NONCUSTODIAL_DAI") + // ); + // core.grantRole( + // VoltRoles.PSM_MINTER, + // addresses.mainnet("PSM_NONCUSTODIAL_USDC") + // ); core.createRole(VoltRoles.PCV_DEPOSIT, VoltRoles.GOVERNOR); - core.grantRole( - VoltRoles.PCV_DEPOSIT, - addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_DAI") - ); - core.grantRole( - VoltRoles.PCV_DEPOSIT, - addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_USDC") - ); - core.grantRole( - VoltRoles.PCV_DEPOSIT, - addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_DAI") - ); - core.grantRole( - VoltRoles.PCV_DEPOSIT, - addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_USDC") - ); + // core.grantRole( + // VoltRoles.PCV_DEPOSIT, + // addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_DAI") + // ); + // core.grantRole( + // VoltRoles.PCV_DEPOSIT, + // addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_USDC") + // ); + // core.grantRole( + // VoltRoles.PCV_DEPOSIT, + // addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_DAI") + // ); + // core.grantRole( + // VoltRoles.PCV_DEPOSIT, + // addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_USDC") + // ); core.grantRole( VoltRoles.PCV_DEPOSIT, addresses.mainnet("PCV_DEPOSIT_EULER_DAI") @@ -423,41 +361,41 @@ contract vip16 is Proposal { core.grantGuardian(addresses.mainnet("GOVERNOR")); /// team multisig core.grantGuardian(addresses.mainnet("COMPOUND_BAD_DEBT_SENTINEL")); - core.grantPsmMinter(addresses.mainnet("PSM_NONCUSTODIAL_DAI")); - core.grantPsmMinter(addresses.mainnet("PSM_NONCUSTODIAL_USDC")); + // core.grantPsmMinter(addresses.mainnet("PSM_NONCUSTODIAL_DAI")); + // core.grantPsmMinter(addresses.mainnet("PSM_NONCUSTODIAL_USDC")); core.grantLocker(addresses.mainnet("SYSTEM_ENTRY")); core.grantLocker(addresses.mainnet("PCV_ORACLE")); - core.grantLocker(addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_DAI")); - core.grantLocker(addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_USDC")); + // core.grantLocker(addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_DAI")); + // core.grantLocker(addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_USDC")); core.grantLocker(addresses.mainnet("GLOBAL_RATE_LIMITED_MINTER")); core.grantLocker(addresses.mainnet("PCV_ROUTER")); core.grantLocker(addresses.mainnet("PCV_GUARDIAN")); - core.grantLocker(addresses.mainnet("PSM_NONCUSTODIAL_DAI")); - core.grantLocker(addresses.mainnet("PSM_NONCUSTODIAL_USDC")); + // core.grantLocker(addresses.mainnet("PSM_NONCUSTODIAL_DAI")); + // core.grantLocker(addresses.mainnet("PSM_NONCUSTODIAL_USDC")); core.grantLocker(addresses.mainnet("PCV_DEPOSIT_EULER_DAI")); core.grantLocker(addresses.mainnet("PCV_DEPOSIT_EULER_USDC")); - core.grantLocker(addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_DAI")); - core.grantLocker(addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_USDC")); + // core.grantLocker(addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_DAI")); + // core.grantLocker(addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_USDC")); core.grantMinter(addresses.mainnet("GLOBAL_RATE_LIMITED_MINTER")); /// Configure PCV Oracle - address[] memory venues = new address[](6); - venues[0] = addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_DAI"); - venues[1] = addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_USDC"); - venues[2] = addresses.mainnet("PCV_DEPOSIT_EULER_DAI"); - venues[3] = addresses.mainnet("PCV_DEPOSIT_EULER_USDC"); - venues[4] = addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_DAI"); - venues[5] = addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_USDC"); - - address[] memory oracles = new address[](6); + address[] memory venues = new address[](2); + // venues[0] = addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_DAI"); + // venues[1] = addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_USDC"); + venues[0] = addresses.mainnet("PCV_DEPOSIT_EULER_DAI"); + venues[1] = addresses.mainnet("PCV_DEPOSIT_EULER_USDC"); + // venues[4] = addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_DAI"); + // venues[5] = addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_USDC"); + + address[] memory oracles = new address[](2); oracles[0] = addresses.mainnet("ORACLE_CONSTANT_DAI"); oracles[1] = addresses.mainnet("ORACLE_CONSTANT_USDC"); - oracles[2] = addresses.mainnet("ORACLE_CONSTANT_DAI"); - oracles[3] = addresses.mainnet("ORACLE_CONSTANT_USDC"); - oracles[4] = addresses.mainnet("ORACLE_CONSTANT_DAI"); - oracles[5] = addresses.mainnet("ORACLE_CONSTANT_USDC"); + // oracles[2] = addresses.mainnet("ORACLE_CONSTANT_DAI"); + // oracles[3] = addresses.mainnet("ORACLE_CONSTANT_USDC"); + // oracles[4] = addresses.mainnet("ORACLE_CONSTANT_DAI"); + // oracles[5] = addresses.mainnet("ORACLE_CONSTANT_USDC"); pcvOracle.addVenues(venues, oracles); @@ -540,20 +478,20 @@ contract vip16 is Proposal { address(CoreRefV2(addresses.mainnet("VOLT_SYSTEM_ORACLE")).core()), address(core) ); - assertEq( - address( - CoreRefV2(addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_DAI")) - .core() - ), - address(core) - ); - assertEq( - address( - CoreRefV2(addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_USDC")) - .core() - ), - address(core) - ); + // assertEq( + // address( + // CoreRefV2(addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_DAI")) + // .core() + // ), + // address(core) + // ); + // assertEq( + // address( + // CoreRefV2(addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_USDC")) + // .core() + // ), + // address(core) + // ); assertEq( address( CoreRefV2(addresses.mainnet("PCV_DEPOSIT_EULER_DAI")).core() @@ -566,32 +504,32 @@ contract vip16 is Proposal { ), address(core) ); - assertEq( - address( - CoreRefV2(addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_DAI")) - .core() - ), - address(core) - ); - assertEq( - address( - CoreRefV2(addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_USDC")) - .core() - ), - address(core) - ); - assertEq( - address( - CoreRefV2(addresses.mainnet("PSM_NONCUSTODIAL_USDC")).core() - ), - address(core) - ); - assertEq( - address( - CoreRefV2(addresses.mainnet("PSM_NONCUSTODIAL_DAI")).core() - ), - address(core) - ); + // assertEq( + // address( + // CoreRefV2(addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_DAI")) + // .core() + // ), + // address(core) + // ); + // assertEq( + // address( + // CoreRefV2(addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_USDC")) + // .core() + // ), + // address(core) + // ); + // assertEq( + // address( + // CoreRefV2(addresses.mainnet("PSM_NONCUSTODIAL_USDC")).core() + // ), + // address(core) + // ); + // assertEq( + // address( + // CoreRefV2(addresses.mainnet("PSM_NONCUSTODIAL_DAI")).core() + // ), + // address(core) + // ); // V1_MIGRATION_ROUTER is not CoreRef, it is only a util contract to call other contracts assertEq( address(CoreRefV2(addresses.mainnet("SYSTEM_ENTRY")).core()), @@ -643,31 +581,31 @@ contract vip16 is Proposal { assertEq(address(core.pcvOracle()), addresses.mainnet("PCV_ORACLE")); // pcv oracle - assertEq(pcvOracle.getVenues().length, 6); + assertEq(pcvOracle.getVenues().length, 2); + // assertEq( + // pcvOracle.getVenues()[0], + // addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_DAI") + // ); + // assertEq( + // pcvOracle.getVenues()[1], + // addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_USDC") + // ); assertEq( pcvOracle.getVenues()[0], - addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_DAI") - ); - assertEq( - pcvOracle.getVenues()[1], - addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_USDC") - ); - assertEq( - pcvOracle.getVenues()[2], addresses.mainnet("PCV_DEPOSIT_EULER_DAI") ); assertEq( - pcvOracle.getVenues()[3], + pcvOracle.getVenues()[1], addresses.mainnet("PCV_DEPOSIT_EULER_USDC") ); - assertEq( - pcvOracle.getVenues()[4], - addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_DAI") - ); - assertEq( - pcvOracle.getVenues()[5], - addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_USDC") - ); + // assertEq( + // pcvOracle.getVenues()[4], + // addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_DAI") + // ); + // assertEq( + // pcvOracle.getVenues()[5], + // addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_USDC") + // ); // pcv router assertTrue( @@ -675,31 +613,31 @@ contract vip16 is Proposal { ); // oracle references - assertEq( - address( - IOracleRefV2(addresses.mainnet("PSM_NONCUSTODIAL_DAI")).oracle() - ), - addresses.mainnet("VOLT_SYSTEM_ORACLE") - ); - assertEq( - address( - IOracleRefV2(addresses.mainnet("PSM_NONCUSTODIAL_USDC")) - .oracle() - ), - addresses.mainnet("VOLT_SYSTEM_ORACLE") - ); + // assertEq( + // address( + // IOracleRefV2(addresses.mainnet("PSM_NONCUSTODIAL_DAI")).oracle() + // ), + // addresses.mainnet("VOLT_SYSTEM_ORACLE") + // ); + // assertEq( + // address( + // IOracleRefV2(addresses.mainnet("PSM_NONCUSTODIAL_USDC")) + // .oracle() + // ), + // addresses.mainnet("VOLT_SYSTEM_ORACLE") + // ); /// compound bad debt sentinel - assertTrue( - badDebtSentinel.isCompoundPcvDeposit( - addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_USDC") - ) - ); - assertTrue( - badDebtSentinel.isCompoundPcvDeposit( - addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_DAI") - ) - ); + // assertTrue( + // badDebtSentinel.isCompoundPcvDeposit( + // addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_USDC") + // ) + // ); + // assertTrue( + // badDebtSentinel.isCompoundPcvDeposit( + // addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_DAI") + // ) + // ); assertEq( badDebtSentinel.pcvGuardian(), addresses.mainnet("PCV_GUARDIAN") diff --git a/test/proposals/vips/vip17.sol b/test/proposals/vips/vip17.sol index 1cf0eccf1..bf3b5c064 100644 --- a/test/proposals/vips/vip17.sol +++ b/test/proposals/vips/vip17.sol @@ -27,7 +27,7 @@ contract vip17 is MultisigProposal { // We expect a 100% PCV change in the PCV Oracle for this proposal, because // before this proposal, PCV oracle has 0 PCV, but after, it will list all // the protocol's funds. - EXPECT_PCV_CHANGE = 1e18; + // EXPECT_PCV_CHANGE = 1e18; // We changed the way oracles are handled in PSMs (value is inverted, and // the decimals are handled differently), so skip the PSM oracle test. SKIP_PSM_ORACLE_TEST = true; diff --git a/test/unit/pcv/compound/CompoundBadDebtSentinel.t.sol b/test/unit/pcv/compound/CompoundBadDebtSentinel.t.sol deleted file mode 100644 index 8dc736b1b..000000000 --- a/test/unit/pcv/compound/CompoundBadDebtSentinel.t.sol +++ /dev/null @@ -1,339 +0,0 @@ -pragma solidity =0.8.13; - -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; - -import {Test} from "@forge-std/Test.sol"; -import {CoreV2} from "@voltprotocol/core/CoreV2.sol"; -import {stdError} from "@forge-std/StdError.sol"; -import {getCoreV2} from "@test/unit/utils/Fixtures.sol"; -import {MockERC20} from "@test/mock/MockERC20.sol"; -import {MockCToken} from "@test/mock/MockCToken.sol"; -import {MockMorpho} from "@test/mock/MockMorpho.sol"; -import {IPCVOracle} from "@voltprotocol/oracle/IPCVOracle.sol"; -import {IPCVDeposit} from "@voltprotocol/pcv/IPCVDeposit.sol"; -import {PCVGuardian} from "@voltprotocol/pcv/PCVGuardian.sol"; -import {SystemEntry} from "@voltprotocol/entry/SystemEntry.sol"; -import {GenericCallMock} from "@test/mock/GenericCallMock.sol"; -import {MockPCVDepositV3} from "@test/mock/MockPCVDepositV3.sol"; -import {MockERC20, IERC20} from "@test/mock/MockERC20.sol"; -import {CompoundBadDebtSentinel} from "@voltprotocol/pcv/compound/CompoundBadDebtSentinel.sol"; -import {MorphoCompoundPCVDeposit} from "@voltprotocol/pcv/morpho/MorphoCompoundPCVDeposit.sol"; -import {TestAddresses as addresses} from "@test/unit/utils/TestAddresses.sol"; -import {MockMorphoMaliciousReentrancy} from "@test/mock/MockMorphoMaliciousReentrancy.sol"; -import {IGlobalReentrancyLock, GlobalReentrancyLock} from "@voltprotocol/core/GlobalReentrancyLock.sol"; - -contract UnitTestCompoundBadDebtSentinel is Test { - using SafeCast for *; - - event Deposit(address indexed _from, uint256 _amount); - - event Harvest(address indexed _token, int256 _profit, uint256 _timestamp); - - event Withdrawal( - address indexed _caller, - address indexed _to, - uint256 _amount - ); - - CoreV2 private core; - SystemEntry private entry; - MockMorpho private morpho; - PCVGuardian private pcvGuardian; - GenericCallMock private comptroller; - MockPCVDepositV3 private safeAddress; - MorphoCompoundPCVDeposit private morphoDeposit; - CompoundBadDebtSentinel private badDebtSentinel; - - uint256 public badDebtThreshold = 1_000_000e18; - - mapping(address => bool) public depositsAdded; - - /// @notice token to deposit - MockERC20 private token; - - /// @notice global reentrancy lock - IGlobalReentrancyLock private lock; - - /// @notice amount to deposit in morpho - uint256 depositAmount = 100_000_000e18; - - function setUp() public { - core = getCoreV2(); - token = new MockERC20(); - lock = IGlobalReentrancyLock( - address(new GlobalReentrancyLock(address(core))) - ); - morpho = new MockMorpho(IERC20(address(token))); - comptroller = new GenericCallMock(); - safeAddress = new MockPCVDepositV3(address(core), address(token)); - - morphoDeposit = new MorphoCompoundPCVDeposit( - address(core), - address(morpho), - address(token), - address(0), - address(morpho), - address(morpho) - ); - address[] memory toWhitelist = new address[](1); - toWhitelist[0] = address(morphoDeposit); - - address[] memory safeAddresslist = new address[](1); - safeAddresslist[0] = address(safeAddress); - - pcvGuardian = new PCVGuardian( - address(core), - address(this), - toWhitelist - ); - - badDebtSentinel = new CompoundBadDebtSentinel( - address(core), - address(comptroller), - address(pcvGuardian), - badDebtThreshold - ); - - comptroller.setResponseToCall( - address(0), - "", - abi.encode(uint256(0), uint256(0), uint256(100_000e18)), - bytes4(keccak256("getAccountLiquidity(address)")) - ); - - GenericCallMock mock = new GenericCallMock(); - - vm.startPrank(addresses.governorAddress); - core.grantLocker(address(entry)); - core.grantLocker(address(pcvGuardian)); - core.grantLocker(address(morphoDeposit)); - core.grantPCVController(address(pcvGuardian)); - core.grantPCVGuard(address(this)); - core.grantGuardian(address(badDebtSentinel)); - core.setGlobalReentrancyLock(lock); - core.setPCVOracle(IPCVOracle(address(mock))); - vm.stopPrank(); - - mock.setResponseToCall( - address(0), - "", - abi.encode(true), - bytes4(keccak256("isVenue(address)")) - ); - - mock.setResponseToCall( - address(0), - "", - abi.encode(uint256(0)), - bytes4(keccak256("lastRecordedPCVRaw(address)")) - ); - - vm.label(address(morpho), "Mock Morpho Market"); - vm.label(address(token), "Token"); - vm.label(address(morphoDeposit), "Morpho PCV Deposit"); - vm.label(address(badDebtSentinel), "Bad Debt Sentinel"); - } - - function testSetup() public { - assertEq(badDebtSentinel.pcvGuardian(), address(pcvGuardian)); - assertEq(badDebtSentinel.comptroller(), address(comptroller)); - assertEq(badDebtSentinel.badDebtThreshold(), badDebtThreshold); - } - - function testNonGovernorCannotUpdateBadDebtThreshold( - uint256 newThreshold - ) public { - vm.expectRevert("CoreRef: Caller is not a governor"); - badDebtSentinel.updateBadDebtThreshold(newThreshold); - } - - function testNonGovernorCannotUpdatePCVGuardian( - address newPcvGuardian - ) public { - vm.expectRevert("CoreRef: Caller is not a governor"); - badDebtSentinel.updatePCVGuardian(newPcvGuardian); - } - - function testNonGovernorCannotAddPCVDeposits( - address[] calldata deposits - ) public { - vm.expectRevert("CoreRef: Caller is not a governor"); - badDebtSentinel.addPCVDeposits(deposits); - } - - function testNonGovernorCannotRemovePCVDeposits( - address[] calldata deposits - ) public { - vm.expectRevert("CoreRef: Caller is not a governor"); - badDebtSentinel.removePCVDeposits(deposits); - } - - function testGovernorCanUpdatePCVGuardian(address newPcvGuardian) public { - vm.prank(addresses.governorAddress); - badDebtSentinel.updatePCVGuardian(newPcvGuardian); - - assertEq(badDebtSentinel.pcvGuardian(), newPcvGuardian); - } - - function testGovernorCanUpdateBadDebtThreshold( - uint256 newThreshold - ) public { - vm.prank(addresses.governorAddress); - badDebtSentinel.updateBadDebtThreshold(newThreshold); - - assertEq(badDebtSentinel.badDebtThreshold(), newThreshold); - } - - function testGovernorCanAddDeposits(address[] calldata deposits) public { - vm.prank(addresses.governorAddress); - badDebtSentinel.addPCVDeposits(deposits); - - for (uint256 i = 0; i < deposits.length; i++) { - assertTrue(badDebtSentinel.isCompoundPcvDeposit(deposits[i])); - } - } - - function _bubbleSort( - address[] memory deposits - ) private pure returns (uint8) { - uint256 depositLength = deposits.length; - if (depositLength == 0) { - return 1; - } - - /// do a bubble sort on input - unchecked { - for (uint256 i = 0; i < depositLength - 1; i++) { - for (uint256 j = 0; j < depositLength - i - 1; j++) { - if (deposits[j] == deposits[j + 1]) { - /// if there are duplicates, return - return 1; - } else if (deposits[j] > deposits[j + 1]) { - address deposit = deposits[j]; - deposits[j] = deposits[j + 1]; - deposits[j + 1] = deposit; - } - } - } - } - - return 0; - } - - function testGovernorCanAddAndThenRemoveDeposits( - address[4] memory depositsToAdd - ) public { - address[] memory deposits = new address[](4); - for (uint i = 0; i < 4; i++) { - deposits[i] = depositsToAdd[i]; - } - - if (_bubbleSort(deposits) == 1) { - return; - } - - vm.prank(addresses.governorAddress); - badDebtSentinel.addPCVDeposits(deposits); - assertEq(badDebtSentinel.allPcvDeposits().length, 4); - - for (uint256 i = 0; i < deposits.length; i++) { - assertTrue(badDebtSentinel.isCompoundPcvDeposit(deposits[i])); - depositsAdded[deposits[i]] = true; - } - - address[] memory retrievedDeposits = badDebtSentinel.allPcvDeposits(); - - for (uint256 i = 0; i < retrievedDeposits.length; i++) { - assertTrue(depositsAdded[retrievedDeposits[i]]); - } - - vm.prank(addresses.governorAddress); - badDebtSentinel.removePCVDeposits(deposits); - assertEq(badDebtSentinel.allPcvDeposits().length, 0); - - for (uint256 i = 0; i < deposits.length; i++) { - assertTrue(!badDebtSentinel.isCompoundPcvDeposit(deposits[i])); - } - } - - function testGetTotalBadDebt(address[] calldata users) public { - uint256 totalBadDebt = badDebtSentinel.getTotalBadDebt(users); - assertEq(totalBadDebt, 100_000e18 * users.length); - } - - function testNoDuplicatesAndOrdered(address[] calldata users) public { - bool isOrdered = true; - for (uint256 i = 0; i < users.length; i++) { - if (depositsAdded[users[i]] == true) { - isOrdered = false; - break; - } - - if (i + 1 < users.length) { - if (users[i] >= users[i + 1]) { - isOrdered = false; - break; - } - } - - depositsAdded[users[i]] = true; - } - - assertEq(isOrdered, badDebtSentinel.noDuplicatesAndOrdered(users)); - } - - function _setupRescue(address[] memory users) private returns (uint8) { - if (_bubbleSort(users) == 1) { - return 1; - } - - address[] memory pcvDeposit = new address[](1); - pcvDeposit[0] = address(morphoDeposit); - - vm.prank(addresses.governorAddress); - badDebtSentinel.addPCVDeposits(pcvDeposit); - - deal(address(token), address(morpho), depositAmount); - morpho.setBalance(address(morphoDeposit), depositAmount); - - return 0; - } - - function testRescueFromCompound(address[] memory users) public { - address[] memory pcvDeposit = new address[](1); - pcvDeposit[0] = address(morphoDeposit); - - if (_setupRescue(users) == 1) { - return; - } - - badDebtSentinel.rescueFromCompound(users, pcvDeposit); - - if (users.length >= 10) { - assertEq(morpho.balances(address(morphoDeposit)), 0); - assertEq(token.balanceOf(address(this)), depositAmount); - } else { - assertEq(morpho.balances(address(morphoDeposit)), depositAmount); - assertEq(token.balanceOf(address(this)), 0); - } - } - - function testRescueAllFromCompound(address[] memory users) public { - address[] memory pcvDeposit = new address[](1); - pcvDeposit[0] = address(morphoDeposit); - - if (_setupRescue(users) == 1) { - return; - } - - badDebtSentinel.rescueAllFromCompound(users); - - if (users.length >= 10) { - assertEq(morpho.balances(address(morphoDeposit)), 0); - assertEq(token.balanceOf(address(this)), depositAmount); - } else { - assertEq(morpho.balances(address(morphoDeposit)), depositAmount); - assertEq(token.balanceOf(address(this)), 0); - } - } -} diff --git a/test/unit/pcv/morpho/MorphoCompoundPCVDeposit.t.sol b/test/unit/pcv/morpho/MorphoCompoundPCVDeposit.t.sol deleted file mode 100644 index 7f6c03e49..000000000 --- a/test/unit/pcv/morpho/MorphoCompoundPCVDeposit.t.sol +++ /dev/null @@ -1,370 +0,0 @@ -pragma solidity =0.8.13; - -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; - -import {Test} from "@forge-std/Test.sol"; -import {CoreV2} from "@voltprotocol/core/CoreV2.sol"; -import {stdError} from "@forge-std/StdError.sol"; -import {MockERC20} from "@test/mock/MockERC20.sol"; -import {MockCToken} from "@test/mock/MockCToken.sol"; -import {GenericCallMock} from "@test/mock/GenericCallMock.sol"; -import {MockMorpho} from "@test/mock/MockMorpho.sol"; -import {IPCVDeposit} from "@voltprotocol/pcv/IPCVDeposit.sol"; -import {PCVGuardian} from "@voltprotocol/pcv/PCVGuardian.sol"; -import {getCoreV2} from "@test/unit/utils/Fixtures.sol"; -import {IPCVOracle} from "@voltprotocol/oracle/IPCVOracle.sol"; -import {SystemEntry} from "@voltprotocol/entry/SystemEntry.sol"; -import {MockERC20, IERC20} from "@test/mock/MockERC20.sol"; -import {MorphoCompoundPCVDeposit} from "@voltprotocol/pcv/morpho/MorphoCompoundPCVDeposit.sol"; -import {TestAddresses as addresses} from "@test/unit/utils/TestAddresses.sol"; -import {MockMorphoMaliciousReentrancy} from "@test/mock/MockMorphoMaliciousReentrancy.sol"; -import {IGlobalReentrancyLock, GlobalReentrancyLock} from "@voltprotocol/core/GlobalReentrancyLock.sol"; - -contract UnitTestMorphoCompoundPCVDeposit is Test { - using SafeCast for *; - - event Deposit(address indexed _from, uint256 _amount); - - event Harvest(address indexed _token, int256 _profit, uint256 _timestamp); - - event Withdrawal( - address indexed _caller, - address indexed _to, - uint256 _amount - ); - - CoreV2 private core; - SystemEntry public entry; - MockMorpho private morpho; - PCVGuardian private pcvGuardian; - MorphoCompoundPCVDeposit private morphoDeposit; - MockMorphoMaliciousReentrancy private maliciousMorpho; - - /// @notice token to deposit - MockERC20 private token; - - /// @notice global reentrancy lock - IGlobalReentrancyLock private lock; - - function setUp() public { - core = getCoreV2(); - token = new MockERC20(); - lock = IGlobalReentrancyLock( - address(new GlobalReentrancyLock(address(core))) - ); - entry = new SystemEntry(address(core)); - morpho = new MockMorpho(IERC20(address(token))); - maliciousMorpho = new MockMorphoMaliciousReentrancy( - IERC20(address(token)) - ); - - morphoDeposit = new MorphoCompoundPCVDeposit( - address(core), - address(morpho), - address(token), - address(0), - address(morpho), - address(morpho) - ); - address[] memory toWhitelist = new address[](1); - toWhitelist[0] = address(morphoDeposit); - - pcvGuardian = new PCVGuardian( - address(core), - address(this), - toWhitelist - ); - GenericCallMock mock = new GenericCallMock(); - - vm.startPrank(addresses.governorAddress); - core.grantLocker(address(entry)); - core.grantLocker(address(pcvGuardian)); - core.grantLocker(address(morphoDeposit)); - core.grantLocker(address(maliciousMorpho)); - core.grantPCVController(address(pcvGuardian)); - core.grantPCVGuard(address(this)); - core.setGlobalReentrancyLock(lock); - core.setPCVOracle(IPCVOracle(address(mock))); - vm.stopPrank(); - - /// everything is a venue - mock.setResponseToCall( - address(0), - "", - abi.encode(true), - bytes4(keccak256("isVenue(address)")) - ); - - mock.setResponseToCall( - address(0), - "", - abi.encode(uint256(0)), - bytes4(keccak256("lastRecordedPCVRaw(address)")) - ); - - vm.label(address(morpho), "MORPHO_COMPOUND"); - vm.label(address(token), "Token"); - vm.label(address(morphoDeposit), "MorphoDeposit"); - - maliciousMorpho.setMorphoCompoundPCVDeposit(address(morphoDeposit)); - } - - function testSetup() public { - assertEq(morphoDeposit.token(), address(token)); - assertEq(morphoDeposit.lens(), address(morpho)); - assertEq(address(morphoDeposit.morpho()), address(morpho)); - assertEq(morphoDeposit.cToken(), address(morpho)); - } - - function testDeposit(uint120 depositAmount) public { - token.mint(address(morphoDeposit), depositAmount); - - entry.deposit(address(morphoDeposit)); - } - - function testDeposits(uint120[4] calldata depositAmount) public { - uint256 sumDeposit; - - for (uint256 i = 0; i < 4; i++) { - token.mint(address(morphoDeposit), depositAmount[i]); - - if (depositAmount[i] != 0) { - vm.expectEmit(true, false, false, true, address(morphoDeposit)); - emit Deposit(address(entry), depositAmount[i]); - } - entry.deposit(address(morphoDeposit)); - - sumDeposit += depositAmount[i]; - } - - assertEq(sumDeposit, morphoDeposit.balance()); - } - - function testWithdrawAll(uint120[4] calldata depositAmount) public { - testDeposits(depositAmount); - - uint256 sumDeposit; - for (uint256 i = 0; i < 4; i++) { - sumDeposit += depositAmount[i]; - } - - assertEq(token.balanceOf(address(this)), 0); - - vm.expectEmit(true, true, false, true, address(morphoDeposit)); - emit Withdrawal(address(pcvGuardian), address(this), sumDeposit); - pcvGuardian.withdrawAllToSafeAddress(address(morphoDeposit)); - - assertEq(token.balanceOf(address(this)), sumDeposit); - assertEq(morphoDeposit.balance(), 0); - } - - function testAccrue( - uint120[4] calldata depositAmount, - uint120 profitAccrued - ) public { - testDeposits(depositAmount); - - uint256 sumDeposit; - for (uint256 i = 0; i < 4; i++) { - sumDeposit += depositAmount[i]; - } - morpho.setBalance(address(morphoDeposit), sumDeposit + profitAccrued); - - if (morphoDeposit.balance() != 0) { - uint256 balance = morphoDeposit.balance(); - vm.expectEmit(true, false, false, true, address(morphoDeposit)); - emit Harvest(address(token), balance.toInt256(), block.timestamp); - } - uint256 lastRecordedBalance = entry.accrue(address(morphoDeposit)); - assertEq(lastRecordedBalance, sumDeposit + profitAccrued); - } - - function testWithdraw( - uint120[4] calldata depositAmount, - uint248[10] calldata withdrawAmount, - uint120 profitAccrued, - address to - ) public { - vm.assume(to != address(0)); - testAccrue(depositAmount, profitAccrued); - token.mint(address(morpho), profitAccrued); /// top up balance so withdraws don't revert - - uint256 sumDeposit = uint256(depositAmount[0]) + - uint256(depositAmount[1]) + - uint256(depositAmount[2]) + - uint256(depositAmount[3]) + - uint256(profitAccrued); - - for (uint256 i = 0; i < 10; i++) { - uint256 amountToWithdraw = withdrawAmount[i]; - if (amountToWithdraw > sumDeposit) { - /// skip if not enough to withdraw - continue; - } - - sumDeposit -= amountToWithdraw; - - uint256 balance = morphoDeposit.balance(); - - vm.expectEmit(true, true, false, true, address(morphoDeposit)); - emit Withdrawal( - address(pcvGuardian), - address(this), - amountToWithdraw - ); - - if (balance != 0) { - emit Harvest( - address(token), - balance.toInt256(), - block.timestamp - ); /// no profits as already accrued - } - - pcvGuardian.withdrawToSafeAddress( - address(morphoDeposit), - amountToWithdraw - ); - } - } - - function testEmergencyActionWithdrawSucceedsGovernor( - uint120 amount - ) public { - token.mint(address(morphoDeposit), amount); - entry.deposit(address(morphoDeposit)); - - MorphoCompoundPCVDeposit.Call[] - memory calls = new MorphoCompoundPCVDeposit.Call[](1); - calls[0].callData = abi.encodeWithSignature( - "withdraw(address,uint256)", - address(this), - amount - ); - calls[0].target = address(morpho); - - vm.prank(addresses.governorAddress); - morphoDeposit.emergencyAction(calls); - - assertEq(morphoDeposit.balance(), 0); - } - - function testEmergencyActionSucceedsGovernorDeposit(uint120 amount) public { - vm.assume(amount != 0); - token.mint(address(morphoDeposit), amount); - - MorphoCompoundPCVDeposit.Call[] - memory calls = new MorphoCompoundPCVDeposit.Call[](2); - calls[0].callData = abi.encodeWithSignature( - "approve(address,uint256)", - address(morphoDeposit.morpho()), - amount - ); - calls[0].target = address(token); - calls[1].callData = abi.encodeWithSignature( - "supply(address,address,uint256)", - address(morphoDeposit), - address(morphoDeposit), - amount - ); - calls[1].target = address(morpho); - - vm.prank(addresses.governorAddress); - morphoDeposit.emergencyAction(calls); - - assertEq(morphoDeposit.balance(), amount); - } - - function testWithdrawFailsOverAmountHeld() public { - vm.expectRevert(stdError.arithmeticError); /// reverts with underflow when trying to withdraw more than balance - pcvGuardian.withdrawToSafeAddress(address(morphoDeposit), 1); - } - - //// paused - - function testDepositWhenPausedFails() public { - vm.prank(addresses.governorAddress); - morphoDeposit.pause(); - vm.expectRevert("Pausable: paused"); - entry.deposit(address(morphoDeposit)); - } - - function testAccrueWhenPausedFails() public { - vm.prank(addresses.governorAddress); - morphoDeposit.pause(); - assertTrue(morphoDeposit.paused()); - vm.expectRevert("Pausable: paused"); - entry.accrue(address(morphoDeposit)); - } - - //// access controls - - function testEmergencyActionFailsNonGovernor() public { - MorphoCompoundPCVDeposit.Call[] - memory calls = new MorphoCompoundPCVDeposit.Call[](1); - calls[0].callData = abi.encodeWithSignature( - "withdraw(address,uint256)", - address(this), - 100 - ); - calls[0].target = address(morpho); - - vm.expectRevert("CoreRef: Caller is not a governor"); - morphoDeposit.emergencyAction(calls); - } - - function testWithdrawFailsNonGovernor() public { - vm.expectRevert("CoreRef: Caller is not a PCV controller"); - morphoDeposit.withdraw(address(this), 100); - } - - function testWithdrawAllFailsNonGovernor() public { - vm.expectRevert("CoreRef: Caller is not a PCV controller"); - morphoDeposit.withdrawAll(address(this)); - } - - //// reentrancy - - function _reentrantSetup() private { - morphoDeposit = new MorphoCompoundPCVDeposit( - address(core), - address(maliciousMorpho), /// cToken is not used in mock morpho deposit - address(token), - address(0), - address(maliciousMorpho), - address(maliciousMorpho) - ); - - vm.prank(addresses.governorAddress); - core.grantLocker(address(morphoDeposit)); - - maliciousMorpho.setMorphoCompoundPCVDeposit(address(morphoDeposit)); - } - - function testReentrantAccrueFails() public { - _reentrantSetup(); - vm.expectRevert("GlobalReentrancyLock: invalid lock level"); - entry.accrue(address(morphoDeposit)); - } - - function testReentrantDepositFails() public { - _reentrantSetup(); - token.mint(address(morphoDeposit), 100); - vm.expectRevert("GlobalReentrancyLock: invalid lock level"); - entry.deposit(address(morphoDeposit)); - } - - function testReentrantWithdrawFails() public { - _reentrantSetup(); - vm.prank(addresses.pcvControllerAddress); - vm.expectRevert("GlobalReentrancyLock: invalid lock level"); - morphoDeposit.withdraw(address(this), 10); - } - - function testReentrantWithdrawAllFails() public { - _reentrantSetup(); - vm.prank(addresses.pcvControllerAddress); - vm.expectRevert("GlobalReentrancyLock: invalid lock level"); - morphoDeposit.withdrawAll(address(this)); - } -} diff --git a/test/unit/refs/OracleRef.t.sol b/test/unit/refs/OracleRef.t.sol index 4ed51ed45..8ef682448 100644 --- a/test/unit/refs/OracleRef.t.sol +++ b/test/unit/refs/OracleRef.t.sol @@ -1,4 +1,4 @@ -/// // SPDX-License-Identifier: GPL-3.0-or-later +/// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.13; import {ICoreV2} from "@voltprotocol/core/ICoreV2.sol"; diff --git a/test/unit/system/System.t.sol b/test/unit/system/System.t.sol deleted file mode 100644 index 38ecb2674..000000000 --- a/test/unit/system/System.t.sol +++ /dev/null @@ -1,490 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.13; - -import {TimelockController} from "@openzeppelin/contracts/governance/TimelockController.sol"; -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -import {console} from "@forge-std/console.sol"; - -import {Test} from "@forge-std/Test.sol"; -import {ICoreV2} from "@voltprotocol/core/ICoreV2.sol"; -import {Deviation} from "@test/unit/utils/Deviation.sol"; -import {VoltRoles} from "@voltprotocol/core/VoltRoles.sol"; -import {MockERC20} from "@test/mock/MockERC20.sol"; -import {PCVRouter} from "@voltprotocol/pcv/PCVRouter.sol"; -import {PCVOracle} from "@voltprotocol/oracle/PCVOracle.sol"; -import {PCVDeposit} from "@voltprotocol/pcv/PCVDeposit.sol"; -import {SystemEntry} from "@voltprotocol/entry/SystemEntry.sol"; -import {PCVGuardian} from "@voltprotocol/pcv/PCVGuardian.sol"; -import {MockCoreRefV2} from "@test/mock/MockCoreRefV2.sol"; -import {GenericCallMock} from "@test/mock/GenericCallMock.sol"; -import {MockPCVDepositV3} from "@test/mock/MockPCVDepositV3.sol"; -import {VoltSystemOracle} from "@voltprotocol/oracle/VoltSystemOracle.sol"; -import {IPCVDepositBalances} from "@voltprotocol/pcv/IPCVDepositBalances.sol"; -import {ConstantPriceOracle} from "@voltprotocol/oracle/ConstantPriceOracle.sol"; -import {MorphoCompoundPCVDeposit} from "@voltprotocol/pcv/morpho/MorphoCompoundPCVDeposit.sol"; -import {TestAddresses as addresses} from "@test/unit/utils/TestAddresses.sol"; -import {IGlobalReentrancyLock, GlobalReentrancyLock} from "@voltprotocol/core/GlobalReentrancyLock.sol"; -import {IGlobalRateLimitedMinter, GlobalRateLimitedMinter} from "@voltprotocol/rate-limits/GlobalRateLimitedMinter.sol"; -import {getCoreV2, getVoltAddresses, VoltAddresses, getVoltSystemOracle} from "@test/unit/utils/Fixtures.sol"; - -/// deployment steps -/// 1. core v2 -/// 2. Volt system oracle -/// 3. oracle pass through -/// 4. peg stability module dai -/// 5. peg stability module usdc -/// 6. pcv deposit dai -/// 7. pcv deposit usdc -/// 8. pcv guardian -/// 9. erc20 allocator - -/// setup steps -/// 1. grant pcv guardian pcv controller role -/// 2. grant erc20 allocator pcv controller role -/// 3. grant pcv guardian guardian role -/// 4. grant pcv guard role to EOA's -/// 5. configure timelock as owner of oracle pass through -/// 6. revoke timelock admin rights from deployer -/// 7. grant timelock governor -/// 8. connect pcv deposits to psm in allocator - -/// PSM target balance is 10k cash for both deposits - -/// test steps -/// 1. do swaps in psm -/// 2. do emergency action to pull funds -/// 3. do sweep to pull funds -/// 4. do pcv guardian withdraw as EOA - -interface IERC20Mintable is IERC20 { - function mint(address, uint256) external; -} - -contract SystemUnitTest is Test { - using SafeCast for *; - VoltAddresses public guardianAddresses = getVoltAddresses(); - - ICoreV2 public core; - SystemEntry public entry; - MockPCVDepositV3 public pcvDepositDai; - MockPCVDepositV3 public pcvDepositUsdc; - PCVGuardian public pcvGuardian; - VoltSystemOracle public oracle; - TimelockController public timelockController; - GlobalRateLimitedMinter public grlm; - IGlobalReentrancyLock public lock; - PCVRouter public pcvRouter; - PCVOracle public pcvOracle; - ConstantPriceOracle public daiConstantOracle; - ConstantPriceOracle public usdcConstantOracle; - - address public voltAddress; - address public coreAddress; - IERC20Mintable public usdc; - IERC20Mintable public dai; - IERC20Mintable public volt; - IERC20Mintable public vcon; - - uint256 public constant timelockDelay = 600; - uint248 public constant usdcTargetBalance = 100_000e6; - uint248 public constant daiTargetBalance = 100_000e18; - int8 public constant usdcDecimalsNormalizer = 12; - int8 public constant daiDecimalsNormalizer = 0; - - /// ---------- GRLM PARAMS ---------- - - /// maximum rate limit per second is 100 VOLT - uint256 public constant maxRateLimitPerSecondMinting = 100e18; - - /// replenish 500k VOLT per day - uint64 public constant rateLimitPerSecondMinting = 5.787e18; - - /// buffer cap of 1.5m VOLT - uint96 public constant bufferCapMinting = 1_500_000e18; - - /// ---------- PSM PARAMS ---------- - - uint128 public constant voltFloorPriceDai = 1.05e18; /// 1 volt for 1.05 dai is the minimum price - uint128 public constant voltCeilingPriceDai = 1.1e18; /// 1 volt for 1.1 dai is the max allowable price - - uint128 public constant voltFloorPriceUsdc = 1.05e6; /// 1 volt for 1.05 usdc is the min price - uint128 public constant voltCeilingPriceUsdc = 1.1e6; /// 1 volt for 1.1 usdc is the max price - - /// ---------- ORACLE PARAMS ---------- - - uint112 public constant startPrice = 1.05e18; - uint112 public constant monthlyChangeRate = .01e18; /// 100 basis points - uint32 public constant startTime = 1_000; - - function setUp() public virtual { - vm.warp(startTime); /// warp past 0 - core = getCoreV2(); - entry = new SystemEntry(address(core)); - volt = IERC20Mintable(address(core.volt())); - vcon = IERC20Mintable(address(core.vcon())); - voltAddress = address(volt); - coreAddress = address(core); - lock = IGlobalReentrancyLock( - address(new GlobalReentrancyLock(coreAddress)) - ); - dai = IERC20Mintable(address(new MockERC20())); - usdc = IERC20Mintable(address(new MockERC20())); - oracle = getVoltSystemOracle( - address(core), - monthlyChangeRate, - startTime, - startPrice - ); - grlm = new GlobalRateLimitedMinter( - coreAddress, - maxRateLimitPerSecondMinting, - rateLimitPerSecondMinting, - bufferCapMinting - ); - - pcvDepositDai = new MockPCVDepositV3(coreAddress, address(dai)); - pcvDepositUsdc = new MockPCVDepositV3(coreAddress, address(usdc)); - - pcvRouter = new PCVRouter(coreAddress); - - pcvOracle = new PCVOracle(coreAddress); - daiConstantOracle = new ConstantPriceOracle(coreAddress, 1e18); - usdcConstantOracle = new ConstantPriceOracle(coreAddress, 1e30); - - address[] memory proposerCancellerAddresses = new address[](3); - proposerCancellerAddresses[0] = guardianAddresses.pcvGuardAddress1; - proposerCancellerAddresses[1] = guardianAddresses.pcvGuardAddress2; - proposerCancellerAddresses[2] = guardianAddresses.executorAddress; - - address[] memory executorAddresses = new address[](2); - executorAddresses[0] = addresses.governorAddress; - executorAddresses[1] = addresses.voltGovernorAddress; - - timelockController = new TimelockController( - timelockDelay, - proposerCancellerAddresses, - executorAddresses - ); - - address[] memory toWhitelist = new address[](2); - toWhitelist[0] = address(pcvDepositDai); - toWhitelist[1] = address(pcvDepositUsdc); - - pcvGuardian = new PCVGuardian( - coreAddress, - address(timelockController), - toWhitelist - ); - - timelockController.renounceRole( - timelockController.TIMELOCK_ADMIN_ROLE(), - address(this) - ); - - vm.startPrank(addresses.governorAddress); - - core.grantPCVController(address(pcvGuardian)); - core.grantPCVController(address(pcvRouter)); - - core.grantPCVGuard(addresses.userAddress); - core.grantPCVGuard(addresses.secondUserAddress); - - core.grantGuardian(address(pcvGuardian)); - - core.grantGovernor(address(timelockController)); - - core.grantMinter(address(grlm)); - - core.grantLocker(address(pcvDepositUsdc)); - core.grantLocker(address(pcvDepositDai)); - core.grantLocker(address(pcvGuardian)); - core.grantLocker(address(pcvOracle)); - core.grantLocker(address(entry)); - core.grantLocker(address(grlm)); - - core.setGlobalRateLimitedMinter( - IGlobalRateLimitedMinter(address(grlm)) - ); - core.setGlobalReentrancyLock(lock); - - /// top up contracts with tokens for testing - /// if done after the setting of pcv oracle, balances will be incorrect unless deposit is called - dai.mint(address(pcvDepositDai), daiTargetBalance); - usdc.mint(address(pcvDepositUsdc), usdcTargetBalance); - - /// Configure PCV Oracle - address[] memory venues = new address[](2); - venues[0] = address(pcvDepositDai); - venues[1] = address(pcvDepositUsdc); - - address[] memory oracles = new address[](2); - oracles[0] = address(daiConstantOracle); - oracles[1] = address(usdcConstantOracle); - - pcvOracle.addVenues(venues, oracles); - - core.setPCVOracle(pcvOracle); - - core.createRole(VoltRoles.PCV_DEPOSIT, VoltRoles.GOVERNOR); - core.grantRole(VoltRoles.PCV_DEPOSIT, address(pcvDepositDai)); - core.grantRole(VoltRoles.PCV_DEPOSIT, address(pcvDepositUsdc)); - - vm.stopPrank(); - - vm.label(address(timelockController), "Timelock Controller"); - vm.label(address(entry), "entry"); - vm.label(address(pcvDepositDai), "pcvDepositDai"); - vm.label(address(pcvDepositUsdc), "pcvDepositUsdc"); - vm.label(address(this), "address this"); - vm.label(address(dai), "DAI"); - vm.label(address(usdc), "USDC"); - } - - function testSetup() public { - assertTrue( - !timelockController.hasRole( - timelockController.TIMELOCK_ADMIN_ROLE(), - address(this) - ) - ); - /// timelock has admin role of itself - assertTrue( - timelockController.hasRole( - timelockController.TIMELOCK_ADMIN_ROLE(), - address(timelockController) - ) - ); - - bytes32 cancellerRole = timelockController.CANCELLER_ROLE(); - assertTrue( - timelockController.hasRole( - cancellerRole, - guardianAddresses.pcvGuardAddress1 - ) - ); - assertTrue( - timelockController.hasRole( - cancellerRole, - guardianAddresses.pcvGuardAddress2 - ) - ); - assertTrue( - timelockController.hasRole( - cancellerRole, - guardianAddresses.executorAddress - ) - ); - - bytes32 proposerRole = timelockController.PROPOSER_ROLE(); - assertTrue( - timelockController.hasRole( - proposerRole, - guardianAddresses.pcvGuardAddress1 - ) - ); - assertTrue( - timelockController.hasRole( - proposerRole, - guardianAddresses.pcvGuardAddress2 - ) - ); - assertTrue( - timelockController.hasRole( - proposerRole, - guardianAddresses.executorAddress - ) - ); - - bytes32 executorRole = timelockController.EXECUTOR_ROLE(); - assertTrue( - timelockController.hasRole(executorRole, addresses.governorAddress) - ); - assertTrue( - timelockController.hasRole( - executorRole, - addresses.voltGovernorAddress - ) - ); - - assertTrue(core.isMinter(address(grlm))); - - assertEq(address(core.globalRateLimitedMinter()), address(grlm)); - - assertTrue(pcvGuardian.isWhitelistAddress(address(pcvDepositDai))); - assertTrue(pcvGuardian.isWhitelistAddress(address(pcvDepositUsdc))); - - assertEq(pcvGuardian.safeAddress(), address(timelockController)); - assertEq(oracle.monthlyChangeRate(), monthlyChangeRate); - - assertTrue(core.isPCVController(address(pcvGuardian))); - - assertTrue(core.isGovernor(address(timelockController))); - assertTrue(core.isGovernor(address(core))); - - assertTrue(core.isPCVGuard(addresses.userAddress)); - assertTrue(core.isPCVGuard(addresses.secondUserAddress)); - - assertTrue(core.isGuardian(address(pcvGuardian))); - - assertTrue(pcvOracle.isVenue(address(pcvDepositDai))); - assertTrue(pcvOracle.isVenue(address(pcvDepositUsdc))); - } - - function testPCVGuardWithdrawAllToSafeAddress() public { - entry.deposit(address(pcvDepositDai)); - entry.deposit(address(pcvDepositUsdc)); - - vm.startPrank(addresses.userAddress); - - pcvGuardian.withdrawAllToSafeAddress(address(pcvDepositDai)); - pcvGuardian.withdrawAllToSafeAddress(address(pcvDepositUsdc)); - - vm.stopPrank(); - - assertEq(dai.balanceOf(address(timelockController)), daiTargetBalance); - assertEq( - usdc.balanceOf(address(timelockController)), - usdcTargetBalance - ); - - assertEq(dai.balanceOf(address(pcvDepositDai)), 0); - assertEq(usdc.balanceOf(address(pcvDepositUsdc)), 0); - } - - function _emergencyPause() private { - vm.prank(addresses.governorAddress); - lock.governanceEmergencyPause(); - - assertEq(lock.lockLevel(), 2); - assertTrue(lock.isLocked()); - assertTrue(!lock.isUnlocked()); - } - - function testPcvGuardianFailureOnSystemEmergencyPause() public { - _emergencyPause(); - - vm.prank(addresses.userAddress); - vm.expectRevert("GlobalReentrancyLock: invalid lock level"); - pcvGuardian.withdrawAllERC20ToSafeAddress( - address(pcvDepositDai), - address(dai) - ); - - vm.prank(addresses.userAddress); - vm.expectRevert("GlobalReentrancyLock: invalid lock level"); - pcvGuardian.withdrawERC20ToSafeAddress( - address(pcvDepositDai), - address(dai), - 0 - ); - - vm.prank(addresses.userAddress); - vm.expectRevert("GlobalReentrancyLock: invalid lock level"); - pcvGuardian.withdrawToSafeAddress(address(pcvDepositDai), 0); - - vm.prank(addresses.userAddress); - vm.expectRevert("GlobalReentrancyLock: invalid lock level"); - pcvGuardian.withdrawAllToSafeAddress(address(pcvDepositDai)); - } - - function testReentrancyLockMaliciousExternalVenue() public { - /// fake deposit - GenericCallMock mock = new GenericCallMock(); - - mock.setResponseToCall( - address(0), - "", - abi.encode(usdc), - IPCVDepositBalances.balanceReportedIn.selector - ); - - /// ctoken response - mock.setResponseToCall( - address(0), - "", - abi.encode(usdc), - bytes4(keccak256("underlying()")) - ); - - /// morpho lens response - mock.setResponseToCall( - address(0), - "", - abi.encode(usdc), - bytes4(keccak256("getCurrentSupplyBalanceInOf(address,address)")) - ); - - /// morpho response - mock.setResponseToCall( - address(0), - "", - "", - bytes4(keccak256("supply(address,address,uint256)")) - ); - - MorphoCompoundPCVDeposit deposit = new MorphoCompoundPCVDeposit( - address(core), - address(mock), - address(usdc), - address(0), - address(mock), - address(mock) - ); - vm.label(address(deposit), "Malicious Morpho Compound PCV Deposit"); - - /// Configure PCV Oracle - address[] memory venues = new address[](1); - venues[0] = address(deposit); - - address[] memory oracles = new address[](1); - oracles[0] = address(usdcConstantOracle); - - /// call to morpo update indexes does nothing at first - mock.setResponseToCall( - address(0), - "", - "", - bytes4(keccak256("updateP2PIndexes(address)")) - ); - - /// call to accrue returns 0 - mock.setResponseToCall( - address(0), - "", - abi.encode(0), - bytes4(keccak256("accrue(address)")) - ); - - /// call to getCurrentSupplyBalanceInOf returns 0 - mock.setResponseToCall( - address(0), - "", - abi.encode(0, 0, 0), - bytes4(keccak256("getCurrentSupplyBalanceInOf(address,address)")) - ); - - vm.startPrank(addresses.governorAddress); - core.grantLocker(address(deposit)); - core.createRole(VoltRoles.PCV_DEPOSIT, VoltRoles.GOVERNOR); - core.grantRole(VoltRoles.PCV_DEPOSIT, address(deposit)); - pcvOracle.addVenues(venues, oracles); - vm.stopPrank(); - - /// call to morpo update indexes attempts reentry - mock.setResponseToCall( - address(entry), - abi.encodeWithSignature("accrue(address)", address(deposit)), - "", - bytes4(keccak256("updateP2PIndexes(address)")) - ); - - vm.expectRevert("GlobalReentrancyLock: invalid lock level"); - entry.accrue(address(deposit)); - - deal(address(usdc), address(deposit), 1); - vm.expectRevert("GlobalReentrancyLock: invalid lock level"); - entry.deposit(address(deposit)); - } -} From d2138e93431d6f054a957b3f10c26c98b27e7d5b Mon Sep 17 00:00:00 2001 From: Elliot Date: Wed, 1 Mar 2023 12:19:34 -0700 Subject: [PATCH 71/74] remove Euler PCV Deposit --- src/pcv/euler/EulerPCVDeposit.sol | 77 ------------------ src/pcv/euler/IEToken.sol | 21 ----- test/proposals/vips/vip16.sol | 130 +++++++++++++++--------------- 3 files changed, 65 insertions(+), 163 deletions(-) delete mode 100644 src/pcv/euler/EulerPCVDeposit.sol delete mode 100644 src/pcv/euler/IEToken.sol diff --git a/src/pcv/euler/EulerPCVDeposit.sol b/src/pcv/euler/EulerPCVDeposit.sol deleted file mode 100644 index 96abe9c91..000000000 --- a/src/pcv/euler/EulerPCVDeposit.sol +++ /dev/null @@ -1,77 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity =0.8.13; - -import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -import {IEToken} from "@voltprotocol/pcv/euler/IEToken.sol"; -import {CoreRefV2} from "@voltprotocol/refs/CoreRefV2.sol"; -import {PCVDepositV2} from "@voltprotocol/pcv/PCVDepositV2.sol"; - -/// @notice PCV Deposit for Euler -contract EulerPCVDeposit is PCVDepositV2 { - using SafeERC20 for IERC20; - - /// @notice euler main contract that receives tokens - address public immutable eulerMain; - - /// @notice reference to the euler token that represents an asset - IEToken public immutable eToken; - - /// @notice sub-account id for euler - uint256 public constant subAccountId = 0; - - /// @notice fetch underlying asset by calling pool and getting liquidity asset - /// @param _core reference to the Core contract - /// @param _eToken reference to the euler asset token - /// @param _eulerMain reference to the euler main address - constructor( - address _core, - address _eToken, - address _eulerMain, - address _underlying, - address _rewardToken - ) PCVDepositV2(_underlying, _rewardToken) CoreRefV2(_core) { - eToken = IEToken(_eToken); - eulerMain = _eulerMain; - address underlying = IEToken(_eToken).underlyingAsset(); - - require(underlying == _underlying, "EulerDeposit: underlying mismatch"); - } - - /// @notice return the amount of funds this contract owns in underlying tokens - function balance() public view override returns (uint256) { - return eToken.balanceOfUnderlying(address(this)); - } - - /// @notice deposit PCV into Euler. - function _supply(uint256 amount) internal override { - /// approve euler main to spend underlying token - IERC20(token).approve(eulerMain, amount); - /// deposit into eToken - eToken.deposit(subAccountId, amount); - } - - /// @notice withdraw PCV from Euler, only callable by PCV controller - /// @param to destination after funds are withdrawn from venue - /// @param amount of PCV to withdraw from the venue - function _withdrawAndTransfer( - uint256 amount, - address to - ) internal override { - eToken.withdraw(subAccountId, amount); - IERC20(token).safeTransfer(to, amount); - } - - /// @notice this is a no-op - /// euler distributes tokens through a merkle drop, - /// no need for claim functionality - function _claim() internal pure override returns (uint256) { - return 0; - } - - /// @notice this is a no-op - /// euler automatically gets the most up to date balance of each user - /// and does not require any poking - function _accrueUnderlying() internal override {} -} diff --git a/src/pcv/euler/IEToken.sol b/src/pcv/euler/IEToken.sol deleted file mode 100644 index 236a79140..000000000 --- a/src/pcv/euler/IEToken.sol +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity =0.8.13; - -interface IEToken { - /// @notice withdraw from euler - function withdraw(uint256 subAccountId, uint256 amount) external; - - /// @notice deposit into euler - function deposit(uint256 subAccountId, uint256 amount) external; - - /// @notice returns balance of underlying including all interest accrued - function balanceOfUnderlying( - address account - ) external view returns (uint256); - - /// @notice returns address of underlying token - function underlyingAsset() external view returns (address); - - /// @notice returns balance of address - function balanceOf(address) external view returns (uint256); -} diff --git a/test/proposals/vips/vip16.sol b/test/proposals/vips/vip16.sol index 79acbaa2b..a760eabbf 100644 --- a/test/proposals/vips/vip16.sol +++ b/test/proposals/vips/vip16.sol @@ -24,7 +24,7 @@ import {IPCVDepositV2} from "@voltprotocol/pcv/IPCVDepositV2.sol"; import {NonCustodialPSM} from "@voltprotocol/peg/NonCustodialPSM.sol"; import {MakerPCVSwapper} from "@voltprotocol/pcv/maker/MakerPCVSwapper.sol"; import {VoltSystemOracle} from "@voltprotocol/oracle/VoltSystemOracle.sol"; -import {EulerPCVDeposit} from "@voltprotocol/pcv/euler/EulerPCVDeposit.sol"; +// import {EulerPCVDeposit} from "@voltprotocol/pcv/euler/EulerPCVDeposit.sol"; import {IPegStabilityModule} from "@voltprotocol/peg/IPegStabilityModule.sol"; import {ConstantPriceOracle} from "@voltprotocol/oracle/ConstantPriceOracle.sol"; import {CompoundBadDebtSentinel} from "@voltprotocol/pcv/compound/CompoundBadDebtSentinel.sol"; @@ -127,33 +127,33 @@ contract vip16 is Proposal { ); addresses.addMainnet("GLOBAL_RATE_LIMITED_MINTER", address(grlm)); } - { - /// Euler Deposits - EulerPCVDeposit eulerDaiPCVDeposit = new EulerPCVDeposit( - addresses.mainnet("CORE"), - addresses.mainnet("EULER_DAI"), - addresses.mainnet("EULER_MAIN"), - addresses.mainnet("DAI"), - address(0) - ); - - EulerPCVDeposit eulerUsdcPCVDeposit = new EulerPCVDeposit( - addresses.mainnet("CORE"), - addresses.mainnet("EULER_USDC"), - addresses.mainnet("EULER_MAIN"), - addresses.mainnet("USDC"), - address(0) - ); - - addresses.addMainnet( - "PCV_DEPOSIT_EULER_DAI", - address(eulerDaiPCVDeposit) - ); - addresses.addMainnet( - "PCV_DEPOSIT_EULER_USDC", - address(eulerUsdcPCVDeposit) - ); - } + // { + // /// Euler Deposits + // EulerPCVDeposit eulerDaiPCVDeposit = new EulerPCVDeposit( + // addresses.mainnet("CORE"), + // addresses.mainnet("EULER_DAI"), + // addresses.mainnet("EULER_MAIN"), + // addresses.mainnet("DAI"), + // address(0) + // ); + + // EulerPCVDeposit eulerUsdcPCVDeposit = new EulerPCVDeposit( + // addresses.mainnet("CORE"), + // addresses.mainnet("EULER_USDC"), + // addresses.mainnet("EULER_MAIN"), + // addresses.mainnet("USDC"), + // address(0) + // ); + + // addresses.addMainnet( + // "PCV_DEPOSIT_EULER_DAI", + // address(eulerDaiPCVDeposit) + // ); + // addresses.addMainnet( + // "PCV_DEPOSIT_EULER_USDC", + // address(eulerUsdcPCVDeposit) + // ); + // } /// VOLT rate { @@ -344,14 +344,14 @@ contract vip16 is Proposal { // VoltRoles.PCV_DEPOSIT, // addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_USDC") // ); - core.grantRole( - VoltRoles.PCV_DEPOSIT, - addresses.mainnet("PCV_DEPOSIT_EULER_DAI") - ); - core.grantRole( - VoltRoles.PCV_DEPOSIT, - addresses.mainnet("PCV_DEPOSIT_EULER_USDC") - ); + // core.grantRole( + // VoltRoles.PCV_DEPOSIT, + // addresses.mainnet("PCV_DEPOSIT_EULER_DAI") + // ); + // core.grantRole( + // VoltRoles.PCV_DEPOSIT, + // addresses.mainnet("PCV_DEPOSIT_EULER_USDC") + // ); core.grantPCVGuard(addresses.mainnet("EOA_1")); core.grantPCVGuard(addresses.mainnet("EOA_2")); @@ -373,25 +373,25 @@ contract vip16 is Proposal { core.grantLocker(addresses.mainnet("PCV_GUARDIAN")); // core.grantLocker(addresses.mainnet("PSM_NONCUSTODIAL_DAI")); // core.grantLocker(addresses.mainnet("PSM_NONCUSTODIAL_USDC")); - core.grantLocker(addresses.mainnet("PCV_DEPOSIT_EULER_DAI")); - core.grantLocker(addresses.mainnet("PCV_DEPOSIT_EULER_USDC")); + // core.grantLocker(addresses.mainnet("PCV_DEPOSIT_EULER_DAI")); + // core.grantLocker(addresses.mainnet("PCV_DEPOSIT_EULER_USDC")); // core.grantLocker(addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_DAI")); // core.grantLocker(addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_USDC")); core.grantMinter(addresses.mainnet("GLOBAL_RATE_LIMITED_MINTER")); /// Configure PCV Oracle - address[] memory venues = new address[](2); + address[] memory venues = new address[](0); // venues[0] = addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_DAI"); // venues[1] = addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_USDC"); - venues[0] = addresses.mainnet("PCV_DEPOSIT_EULER_DAI"); - venues[1] = addresses.mainnet("PCV_DEPOSIT_EULER_USDC"); + // venues[0] = addresses.mainnet("PCV_DEPOSIT_EULER_DAI"); + // venues[1] = addresses.mainnet("PCV_DEPOSIT_EULER_USDC"); // venues[4] = addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_DAI"); // venues[5] = addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_USDC"); - address[] memory oracles = new address[](2); - oracles[0] = addresses.mainnet("ORACLE_CONSTANT_DAI"); - oracles[1] = addresses.mainnet("ORACLE_CONSTANT_USDC"); + address[] memory oracles = new address[](0); + // oracles[0] = addresses.mainnet("ORACLE_CONSTANT_DAI"); + // oracles[1] = addresses.mainnet("ORACLE_CONSTANT_USDC"); // oracles[2] = addresses.mainnet("ORACLE_CONSTANT_DAI"); // oracles[3] = addresses.mainnet("ORACLE_CONSTANT_USDC"); // oracles[4] = addresses.mainnet("ORACLE_CONSTANT_DAI"); @@ -492,18 +492,18 @@ contract vip16 is Proposal { // ), // address(core) // ); - assertEq( - address( - CoreRefV2(addresses.mainnet("PCV_DEPOSIT_EULER_DAI")).core() - ), - address(core) - ); - assertEq( - address( - CoreRefV2(addresses.mainnet("PCV_DEPOSIT_EULER_USDC")).core() - ), - address(core) - ); + // assertEq( + // address( + // CoreRefV2(addresses.mainnet("PCV_DEPOSIT_EULER_DAI")).core() + // ), + // address(core) + // ); + // assertEq( + // address( + // CoreRefV2(addresses.mainnet("PCV_DEPOSIT_EULER_USDC")).core() + // ), + // address(core) + // ); // assertEq( // address( // CoreRefV2(addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_DAI")) @@ -581,7 +581,7 @@ contract vip16 is Proposal { assertEq(address(core.pcvOracle()), addresses.mainnet("PCV_ORACLE")); // pcv oracle - assertEq(pcvOracle.getVenues().length, 2); + assertEq(pcvOracle.getVenues().length, 0); // assertEq( // pcvOracle.getVenues()[0], // addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_DAI") @@ -590,14 +590,14 @@ contract vip16 is Proposal { // pcvOracle.getVenues()[1], // addresses.mainnet("PCV_DEPOSIT_MORPHO_COMPOUND_USDC") // ); - assertEq( - pcvOracle.getVenues()[0], - addresses.mainnet("PCV_DEPOSIT_EULER_DAI") - ); - assertEq( - pcvOracle.getVenues()[1], - addresses.mainnet("PCV_DEPOSIT_EULER_USDC") - ); + // assertEq( + // pcvOracle.getVenues()[0], + // addresses.mainnet("PCV_DEPOSIT_EULER_DAI") + // ); + // assertEq( + // pcvOracle.getVenues()[1], + // addresses.mainnet("PCV_DEPOSIT_EULER_USDC") + // ); // assertEq( // pcvOracle.getVenues()[4], // addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_DAI") From 7dd04fd90bfe815b7160236c1ac25fbf4fcd1dea Mon Sep 17 00:00:00 2001 From: Elliot Date: Wed, 1 Mar 2023 12:21:27 -0700 Subject: [PATCH 72/74] todo comments --- .../proposal-checks/IntegrationTestProposalNoPCVLeak.sol | 9 +++++---- test/proposals/vips/vip16.sol | 2 ++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/test/integration/proposal-checks/IntegrationTestProposalNoPCVLeak.sol b/test/integration/proposal-checks/IntegrationTestProposalNoPCVLeak.sol index 73e7c0465..6aacef863 100644 --- a/test/integration/proposal-checks/IntegrationTestProposalNoPCVLeak.sol +++ b/test/integration/proposal-checks/IntegrationTestProposalNoPCVLeak.sol @@ -67,9 +67,10 @@ contract IntegrationTestProposalNoPCVLeak is Test { ); emit log_named_int("proposalLeakedPcv ", proposalLeakedPcv); } - assertTrue( - proposalLeakedPcv < int256(toleratedPcvLoss), - "PCV Leak in proposals" - ); + /// TODO fix once deposits are added back + // assertTrue( + // proposalLeakedPcv < int256(toleratedPcvLoss), + // "PCV Leak in proposals" + // ); } } diff --git a/test/proposals/vips/vip16.sol b/test/proposals/vips/vip16.sol index a760eabbf..c5ee2781a 100644 --- a/test/proposals/vips/vip16.sol +++ b/test/proposals/vips/vip16.sol @@ -37,6 +37,8 @@ VIP16 does not do any multisig or timelock actions, only deployment of contracts and tying them together properly. */ +/// TODO uncomment pcv deposit checks when things are back online + contract vip16 is Proposal { using SafeCast for *; From 58a3918931b55d3218ac7e4c5ee301fa51230a2f Mon Sep 17 00:00:00 2001 From: Elliot Date: Wed, 1 Mar 2023 12:22:34 -0700 Subject: [PATCH 73/74] test TODO comments --- test/proposals/vips/vip16.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/proposals/vips/vip16.sol b/test/proposals/vips/vip16.sol index c5ee2781a..f9a8eaa89 100644 --- a/test/proposals/vips/vip16.sol +++ b/test/proposals/vips/vip16.sol @@ -37,7 +37,7 @@ VIP16 does not do any multisig or timelock actions, only deployment of contracts and tying them together properly. */ -/// TODO uncomment pcv deposit checks when things are back online +/// TODO uncomment pcv deposit checks when deposits are added back to the repo contract vip16 is Proposal { using SafeCast for *; From b8cb408e4335ce0a93b8e63d6ed8ea4b6c1a070c Mon Sep 17 00:00:00 2001 From: Elliot Date: Wed, 1 Mar 2023 12:23:34 -0700 Subject: [PATCH 74/74] remove PCV Deposit V2 --- src/pcv/PCVDepositV2.sol | 254 --------------------------------------- 1 file changed, 254 deletions(-) delete mode 100644 src/pcv/PCVDepositV2.sol diff --git a/src/pcv/PCVDepositV2.sol b/src/pcv/PCVDepositV2.sol deleted file mode 100644 index dc5baa9d9..000000000 --- a/src/pcv/PCVDepositV2.sol +++ /dev/null @@ -1,254 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.13; - -import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; -import {Address} from "@openzeppelin/contracts/utils/Address.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -import {CoreRefV2} from "@voltprotocol/refs/CoreRefV2.sol"; -import {IPCVDepositV2} from "@voltprotocol/pcv/IPCVDepositV2.sol"; - -/// @title abstract PCV Deposit contract. -/// if PCV Oracle is not set, this contract will revert and not allow operations -/// if global reentrancy lock is not set, this contract will still work. -/// @author Elliot Friedman -abstract contract PCVDepositV2 is IPCVDepositV2, CoreRefV2 { - using SafeERC20 for IERC20; - using SafeCast for *; - - /// ------------------------------------------ - /// --------------- Immutables --------------- - /// ------------------------------------------ - - /// @notice reference to reward token - address public immutable override rewardToken; - - /// @notice reference to underlying token - address public immutable override token; - - /// ------------------------------------------ - /// ------------- State Variables ------------ - /// ------------------------------------------ - - constructor(address _token, address _rewardToken) { - token = _token; - rewardToken = _rewardToken; - } - - /// ------------------------------------------ - /// ----------- Permissionless API ----------- - /// ------------------------------------------ - - /// @notice deposit ERC-20 tokens to underlying venue - /// uses global reentrancy lock to block malicious reentrant - /// state changes to the lastRecordedBalance variable - function deposit() public whenNotPaused globalLock(2) { - /// ------ Check ------ - - uint256 amount = IERC20(token).balanceOf(address(this)); - if (amount == 0) { - /// no op to prevent revert on empty deposit - return; - } - - /// retrieve the non-decimal normalized balance - /// if PCV Oracle is not set, revert - uint256 lastRecordedBalance = pcvOracle().lastRecordedPCVRaw( - address(this) - ); - int256 startingRecordedBalance = lastRecordedBalance.toInt256(); - - /// ------ Effects ------ - - /// compute profit from interest accrued and emit an event - /// if any profits or losses are realized - int256 profit = _recordPNL(); - - /// ------ Interactions ------ - - /// approval and deposit into underlying venue - _supply(amount); - - /// ------ Update Internal Accounting ------ - - int256 endingRecordedBalance = balance().toInt256(); - - _pcvOracleHook(endingRecordedBalance - startingRecordedBalance, profit); - - emit Deposit(msg.sender, amount); - } - - /// @notice claim token rewards for supplying to underlying venue. - /// Does not require reentrancy lock as no internal smart contract - /// state is mutated in this function. - function harvest() external globalLock(2) { - uint256 claimedAmount = _claim(); - - emit Harvest(rewardToken, int256(claimedAmount), block.timestamp); - } - - /// @notice function that emits an event tracking profits and losses - /// since the last contract interaction - /// then writes the current amount of PCV tracked in this contract - /// to lastRecordedBalance - /// @return the amount deposited after adding accrued interest or realizing losses - function accrue() external whenNotPaused globalLock(2) returns (uint256) { - int256 profit = _recordPNL(); /// update deposit amount and fire harvest event - - /// if any amount of PCV is withdrawn and no gains, delta is negative - _pcvOracleHook(profit, profit); - - return balance(); /// return current pcv amount - } - - /// ------------------------------------------ - /// ------------ Permissioned API ------------ - /// ------------------------------------------ - - /// @notice withdraw tokens from the PCV allocation - /// non-reentrant as state changes and external calls are made - /// @param to the address PCV will be sent to - /// @param amount of tokens withdrawn - function withdraw( - address to, - uint256 amount - ) external onlyPCVController globalLock(2) { - int256 profit = _withdraw(to, amount, true); - - /// if any amount of PCV is withdrawn and no gains, delta is negative - _pcvOracleHook(-(amount.toInt256()) + profit, profit); - } - - /// @notice withdraw all tokens from underlying venue - /// global reentrancy locked as state changes and external calls are made - /// @param to the address PCV will be sent to - function withdrawAll(address to) external onlyPCVController globalLock(2) { - /// compute profit from interest accrued and emit an event - int256 profit = _recordPNL(); - - /// retrieve the non-decimal normalized balance - /// if PCV Oracle is not set, revert - uint256 lastRecordedBalance = pcvOracle().lastRecordedPCVRaw( - address(this) - ); - - int256 recordedBalance = lastRecordedBalance.toInt256(); /// cast to int256 - - /// withdraw last recorded amount as this was updated in record pnl - _withdraw(to, lastRecordedBalance, false); - - /// all PCV withdrawn, send call in with amount withdrawn negative if any amount is withdrawn - _pcvOracleHook(-recordedBalance, profit); - } - - /// @notice withdraw ERC20 from the contract - /// @param tokenAddress address of the ERC20 to send - /// @param to address destination of the ERC20 - /// @param amount quantity of ERC20 to send - /// Calling this function will lead to incorrect - /// accounting in a PCV deposit that tracks - /// profits and or last recorded balance. - /// If a deposit records PNL, only use this - /// function in an emergency. - function withdrawERC20( - address tokenAddress, - address to, - uint256 amount - ) public virtual override onlyPCVController { - IERC20(tokenAddress).safeTransfer(to, amount); - emit WithdrawERC20(msg.sender, tokenAddress, to, amount); - } - - /// ------------- Internal Helpers ------------- - - /// @notice records how much profit or loss has been accrued - /// since the last call and emits an event with all profit or loss received. - /// Updates the lastRecordedBalance to include all realized profits or losses. - /// @return profit accumulated since last _recordPNL() call. - function _recordPNL() internal returns (int256) { - /// first accrue interest in the underlying venue - _accrueUnderlying(); - - /// ------ Check ------ - - /// then get the current balance from the market - uint256 currentBalance = balance(); - - /// retrieve the non-decimal normalized balance - /// if PCV Oracle is not set, revert - uint256 lastRecordedBalance = pcvOracle().lastRecordedPCVRaw( - address(this) - ); - - /// save gas if contract has no balance - /// if cost basis is 0 and last recorded balance is 0 - /// there is no profit or loss to record and no reason - /// to update lastRecordedBalance - if (currentBalance == 0 && lastRecordedBalance == 0) { - return 0; - } - - /// currentBalance should always be greater than or equal to - /// the deposited amount, except on the same block a deposit - /// occurs, or a loss event in underlying venue. - - /// Compute profit - int256 profit = int256(currentBalance) - int256(lastRecordedBalance); - - /// profit is in underlying token - emit Harvest(token, int256(profit), block.timestamp); - - return profit; - } - - /// @notice helper avoid repeated code in withdraw and withdrawAll - /// anytime this function is called it is by an external function in this smart contract - /// with a reentrancy guard. This ensures lastRecordedBalance never desynchronizes. - /// All current venues are assumed to be loss-less venues. Over the course of less than 1 block, - /// it is possible to lose funds. However, after 1 block, deposits are expected to always - /// be in profit at least with current interest rates around 0.8% natively on Compound, - /// ignoring all other token rewards. - /// @param to recipient of withdraw funds - /// @param amount to withdraw - /// @param recordPnl whether or not to record PnL. Set to false in withdrawAll - /// as the function _recordPNL() is already called before _withdraw - function _withdraw( - address to, - uint256 amount, - bool recordPnl - ) private returns (int256 profit) { - /// ------ Effects ------ - - if (recordPnl) { - /// compute profit from interest accrued and emit a Harvest event - profit = _recordPNL(); - } - - /// ------ Interactions ------ - - /// remove funds from underlying venue - _withdrawAndTransfer(amount, to); - - emit Withdrawal(msg.sender, to, amount); - } - - /// ------------- Virtual Functions ------------- - - /// @notice get balance in the underlying market. - /// @return current balance of deposit - function balance() public view virtual override returns (uint256); - - /// @dev accrue interest in the underlying market. - function _accrueUnderlying() internal virtual; - - /// @dev withdraw from the underlying market. - function _withdrawAndTransfer(uint256 amount, address to) internal virtual; - - /// @dev deposit in the underlying market. - function _supply(uint256 amount) internal virtual; - - /// @dev claim rewards from the underlying market. - /// returns amount of reward tokens claimed - function _claim() internal virtual returns (uint256); -}