diff --git a/src/pcv/ERC20Allocator.sol b/src/pcv/ERC20Allocator.sol index b9e7d253..e791c634 100644 --- a/src/pcv/ERC20Allocator.sol +++ b/src/pcv/ERC20Allocator.sol @@ -216,7 +216,7 @@ contract ERC20Allocator is IERC20Allocator, CoreRefV2 { (uint256 amountToDrip, uint256 adjustedAmountToDrip) = getDripDetails( psm, - pcvDeposit + address(pcvDeposit) ); /// Effects @@ -299,7 +299,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]; @@ -324,7 +324,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/src/pcv/IPCVDepositV2.sol b/src/pcv/IPCVDepositV2.sol index 3cae47bc..42151d31 100644 --- a/src/pcv/IPCVDepositV2.sol +++ b/src/pcv/IPCVDepositV2.sol @@ -1,14 +1,83 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.13; -import {IPCVDeposit} from "@voltprotocol/pcv/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 ----------- + + /// @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; + /// @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 + /// 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 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); + + // ----------- 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/src/pcv/PCVDepositV2.sol b/src/pcv/PCVDepositV2.sol new file mode 100644 index 00000000..72892e14 --- /dev/null +++ b/src/pcv/PCVDepositV2.sol @@ -0,0 +1,275 @@ +// 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 {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 contract for withdrawing 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; + } + + /// ------------------------------------------ + /// -------------- 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 + 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); +} diff --git a/src/pcv/compound/CompoundBadDebtSentinel.sol b/src/pcv/compound/CompoundBadDebtSentinel.sol index cae23ab9..2d7e936e 100644 --- a/src/pcv/compound/CompoundBadDebtSentinel.sol +++ b/src/pcv/compound/CompoundBadDebtSentinel.sol @@ -6,7 +6,7 @@ import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet import {CoreRefV2} from "@voltprotocol/refs/CoreRefV2.sol"; import {PCVDeposit} from "@voltprotocol/pcv/PCVDeposit.sol"; import {PCVGuardian} from "@voltprotocol/pcv/PCVGuardian.sol"; -import {IComptroller} from "@voltprotocol/external/compound/ICompound.sol"; +import {IComptroller} from "@voltprotocol/pcv/morpho/ICompound.sol"; import {ICompoundBadDebtSentinel} from "@voltprotocol/pcv/compound/ICompoundBadDebtSentinel.sol"; /// @notice Contract that removes all funds from Compound diff --git a/src/pcv/euler/EulerPCVDeposit.sol b/src/pcv/euler/EulerPCVDeposit.sol new file mode 100644 index 00000000..96abe9c9 --- /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 00000000..236a7914 --- /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); +} diff --git a/src/external/compound/ICompound.sol b/src/pcv/morpho/ICompound.sol similarity index 100% rename from src/external/compound/ICompound.sol rename to src/pcv/morpho/ICompound.sol diff --git a/src/external/morpho/ILens.sol b/src/pcv/morpho/ILens.sol similarity index 98% rename from src/external/morpho/ILens.sol rename to src/pcv/morpho/ILens.sol index 9a028709..a32e8941 100644 --- a/src/external/morpho/ILens.sol +++ b/src/pcv/morpho/ILens.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: GNU AGPLv3 -pragma solidity ^0.8.0; +pragma solidity =0.8.13; -import "@voltprotocol/external/compound/ICompound.sol"; -import "@voltprotocol/external/morpho/IMorpho.sol"; +import "@voltprotocol/pcv/morpho/ICompound.sol"; +import "@voltprotocol/pcv/morpho/IMorpho.sol"; interface ILens { /// STORAGE /// diff --git a/src/external/morpho/IMorpho.sol b/src/pcv/morpho/IMorpho.sol similarity index 81% rename from src/external/morpho/IMorpho.sol rename to src/pcv/morpho/IMorpho.sol index eabbd05c..f3c64102 100644 --- a/src/external/morpho/IMorpho.sol +++ b/src/pcv/morpho/IMorpho.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GNU AGPLv3 -pragma solidity ^0.8.0; +pragma solidity =0.8.13; -import {IComptroller} from "@voltprotocol/external/compound/ICompound.sol"; +import {IComptroller} from "@voltprotocol/pcv/morpho/ICompound.sol"; // prettier-ignore interface IMorpho { @@ -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; } diff --git a/src/pcv/morpho/MorphoAavePCVDeposit.sol b/src/pcv/morpho/MorphoAavePCVDeposit.sol new file mode 100644 index 00000000..948e38b1 --- /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 + } +} diff --git a/src/pcv/morpho/MorphoCompoundPCVDeposit.sol b/src/pcv/morpho/MorphoCompoundPCVDeposit.sol index 21305276..0b36c34b 100644 --- a/src/pcv/morpho/MorphoCompoundPCVDeposit.sol +++ b/src/pcv/morpho/MorphoCompoundPCVDeposit.sol @@ -2,15 +2,12 @@ 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/external/morpho/ILens.sol"; -import {ICToken} from "@voltprotocol/external/compound/ICompound.sol"; -import {IMorpho} from "@voltprotocol/external/morpho/IMorpho.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 {PCVDeposit} from "@voltprotocol/pcv/PCVDeposit.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 @@ -35,67 +32,38 @@ import {PCVDeposit} from "@voltprotocol/pcv/PCVDeposit.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 MorphoCompoundPCVDeposit is PCVDeposit { +contract MorphoCompoundPCVDeposit is PCVDepositV2 { 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 _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 - ) CoreRefV2(_core) { - if (_underlying != address(Constants.WETH)) { - require( - ICToken(_cToken).underlying() == _underlying, - "MorphoCompoundPCVDeposit: Underlying mismatch" - ); - } + ) PCVDepositV2(_underlying, _rewardToken) CoreRefV2(_core) { cToken = _cToken; - token = _underlying; morpho = _morpho; lens = _lens; } @@ -115,197 +83,41 @@ contract MorphoCompoundPCVDeposit is PCVDeposit { return totalSupplied; } - /// @notice returns the underlying token of this deposit - function balanceReportedIn() external view returns (address) { - return token; - } - /// ------------------------------------------ - /// ----------- Permissionless API ----------- + /// ------------- Helper Methods ------------- /// ------------------------------------------ - /// @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); + /// @notice accrue interest in the underlying morpho venue + function _accrueUnderlying() internal override { + /// accrue interest in Morpho + IMorpho(morpho).updateP2PIndexes(cToken); + } - /// ------ Interactions ------ + /// @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 ); - - 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) { + /// @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; - /// 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 - _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); - } - - /// ------------------------------------------ - /// ------------- 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; + 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 index e962eabb..73b01848 100644 --- a/test/integration/IntegrationTestMorphoCompoundPCVDeposit.t.sol +++ b/test/integration/IntegrationTestMorphoCompoundPCVDeposit.t.sol @@ -3,13 +3,14 @@ pragma solidity =0.8.13; import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {CoreV2} from "@voltprotocol/core/CoreV2.sol"; + import {Test} from "@forge-std/Test.sol"; +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 {PegStabilityModule} from "@voltprotocol/peg/PegStabilityModule.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"; @@ -56,6 +57,7 @@ contract IntegrationTestMorphoCompoundPCVDeposit is Test { address(core), CDAI, DAI, + COMP, MORPHO, MORPHO_LENS ); @@ -64,6 +66,7 @@ contract IntegrationTestMorphoCompoundPCVDeposit is Test { address(core), CUSDC, USDC, + COMP, MORPHO, MORPHO_LENS ); @@ -87,7 +90,7 @@ contract IntegrationTestMorphoCompoundPCVDeposit 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 bcc2fe49..62f895bd 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 00000000..8ff97ccf --- /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 00000000..74fd8fae --- /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 151d445c..7a070392 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 95848354..dfc90483 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 4cefd02e..43acc9fd 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 24c4fbdd..3b793789 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" + ); } /* @@ -214,7 +218,7 @@ contract IntegrationTestRateLimiters is PostProposalCheck { uint256 startingExitBuffer = gserl.buffer(); (, uint256 expectedBufferDepletion) = allocator.getDripDetails( address(daipsm), - PCVDeposit(address(morphoDaiPCVDeposit)) + address(morphoDaiPCVDeposit) ); allocator.drip(address(morphoDaiPCVDeposit)); assertEq(startingExitBuffer - expectedBufferDepletion, gserl.buffer()); @@ -303,7 +307,7 @@ contract IntegrationTestRateLimiters is PostProposalCheck { (, uint256 expectedBufferDepletion) = allocator.getDripDetails( address(usdcpsm), - PCVDeposit(address(morphoUsdcPCVDeposit)) + address(morphoUsdcPCVDeposit) ); allocator.drip(address(morphoUsdcPCVDeposit)); uint256 endingExitBuffer = gserl.buffer(); diff --git a/test/integration/post-proposal-checks/IntegrationTestRoles.sol b/test/integration/post-proposal-checks/IntegrationTestRoles.sol index 14db3225..e6b38b3d 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); diff --git a/test/invariant/InvariantTestMorphoPCVDeposit.t.sol b/test/invariant/InvariantTestMorphoPCVDeposit.t.sol index 34d06644..42fff9f2 100644 --- a/test/invariant/InvariantTestMorphoPCVDeposit.t.sol +++ b/test/invariant/InvariantTestMorphoPCVDeposit.t.sol @@ -4,14 +4,13 @@ pragma solidity =0.8.13; import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; 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 {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 {PCVGuardian} from "@voltprotocol/pcv/PCVGuardian.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"; @@ -35,7 +34,7 @@ contract InvariantTestMorphoCompoundPCVDeposit is Test, InvariantTest { PCVGuardian public pcvGuardian; MockPCVOracle public pcvOracle; IGlobalReentrancyLock private lock; - MorphoPCVDepositTest public morphoTest; + MorphoCompoundPCVDepositTest public morphoTest; MorphoCompoundPCVDeposit public morphoDeposit; function setUp() public { @@ -47,6 +46,7 @@ contract InvariantTestMorphoCompoundPCVDeposit is Test, InvariantTest { address(core), address(morpho), address(token), + address(0), /// no need for reward token address(morpho), address(morpho) ); @@ -64,7 +64,7 @@ contract InvariantTestMorphoCompoundPCVDeposit is Test, InvariantTest { ); entry = new SystemEntry(address(core)); - morphoTest = new MorphoPCVDepositTest( + morphoTest = new MorphoCompoundPCVDepositTest( morphoDeposit, token, morpho, @@ -114,7 +114,7 @@ contract InvariantTestMorphoCompoundPCVDeposit is Test, InvariantTest { } } -contract MorphoPCVDepositTest is Test { +contract MorphoCompoundPCVDepositTest is Test { uint256 public totalDeposited; MockERC20 public token; diff --git a/test/mock/MockMorphoMaliciousReentrancy.sol b/test/mock/MockMorphoMaliciousReentrancy.sol index 7f188625..8fb8bd15 100644 --- a/test/mock/MockMorphoMaliciousReentrancy.sol +++ b/test/mock/MockMorphoMaliciousReentrancy.sol @@ -1,6 +1,7 @@ pragma solidity 0.8.13; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + import {MorphoCompoundPCVDeposit} from "@voltprotocol/pcv/morpho/MorphoCompoundPCVDeposit.sol"; contract MockMorphoMaliciousReentrancy { diff --git a/test/proposals/Addresses.sol b/test/proposals/Addresses.sol index e0b446c8..065cdef1 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 94da07cd..945e8e04 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 22ee8b2c..bd7334b4 100644 --- a/test/proposals/vips/vip16.sol +++ b/test/proposals/vips/vip16.sol @@ -23,12 +23,14 @@ 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 {IPCVDeposit, PCVDeposit} from "@voltprotocol/pcv/PCVDeposit.sol"; -import {MorphoCompoundPCVDeposit} from "@voltprotocol/pcv/morpho/MorphoCompoundPCVDeposit.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"; @@ -149,29 +151,88 @@ contract vip16 is Proposal { /// PCV Deposits { - MorphoCompoundPCVDeposit morphoDaiPCVDeposit = new MorphoCompoundPCVDeposit( + /// Morpho Compound Deposits + MorphoCompoundPCVDeposit morphoCompoundDaiPCVDeposit = new MorphoCompoundPCVDeposit( addresses.mainnet("CORE"), addresses.mainnet("CDAI"), addresses.mainnet("DAI"), - addresses.mainnet("MORPHO"), - addresses.mainnet("MORPHO_LENS") + addresses.mainnet("COMP"), + addresses.mainnet("MORPHO_COMPOUND"), + addresses.mainnet("MORPHO_LENS_COMPOUND") ); - MorphoCompoundPCVDeposit morphoUsdcPCVDeposit = new MorphoCompoundPCVDeposit( + MorphoCompoundPCVDeposit morphoCompoundUsdcPCVDeposit = new MorphoCompoundPCVDeposit( addresses.mainnet("CORE"), addresses.mainnet("CUSDC"), addresses.mainnet("USDC"), - addresses.mainnet("MORPHO"), - addresses.mainnet("MORPHO_LENS") + 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_DAI", - address(morphoDaiPCVDeposit) + "PCV_DEPOSIT_MORPHO_AAVE_DAI", + address(morphoAaveDaiPCVDeposit) ); addresses.addMainnet( - "PCV_DEPOSIT_MORPHO_USDC", - address(morphoUsdcPCVDeposit) + "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("EUSDC"), + 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) ); } @@ -215,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"), @@ -226,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") @@ -279,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"), @@ -412,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_COMPOUND_USDC") ); core.grantRole( VoltRoles.PCV_DEPOSIT, - addresses.mainnet("PCV_DEPOSIT_MORPHO_USDC") + 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")); @@ -455,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")); @@ -479,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); @@ -591,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_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_USDC")).core() + CoreRefV2(addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_DAI")) + .core() + ), + address(core) + ); + assertEq( + address( + CoreRefV2(addresses.mainnet("PCV_DEPOSIT_MORPHO_AAVE_USDC")) + .core() ), address(core) ); @@ -688,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") ); @@ -708,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 @@ -743,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 4bab0f56..1cf0eccf 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" ); diff --git a/test/unit/pcv/compound/CompoundBadDebtSentinel.t.sol b/test/unit/pcv/compound/CompoundBadDebtSentinel.t.sol index 3fb580d4..d2c74951 100644 --- a/test/unit/pcv/compound/CompoundBadDebtSentinel.t.sol +++ b/test/unit/pcv/compound/CompoundBadDebtSentinel.t.sol @@ -2,7 +2,6 @@ pragma solidity =0.8.13; import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; -import {Vm} from "@forge-std/Vm.sol"; import {Test} from "@forge-std/Test.sol"; import {CoreV2} from "@voltprotocol/core/CoreV2.sol"; import {stdError} from "@forge-std/StdError.sol"; @@ -71,6 +70,7 @@ contract UnitTestCompoundBadDebtSentinel is Test { address(core), address(morpho), address(token), + address(0), address(morpho), address(morpho) ); diff --git a/test/unit/pcv/morpho/MorphoCompoundPCVDeposit.t.sol b/test/unit/pcv/morpho/MorphoCompoundPCVDeposit.t.sol index efdbf97a..f8bf2b1e 100644 --- a/test/unit/pcv/morpho/MorphoCompoundPCVDeposit.t.sol +++ b/test/unit/pcv/morpho/MorphoCompoundPCVDeposit.t.sol @@ -2,7 +2,6 @@ pragma solidity =0.8.13; import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; -import {Vm} from "@forge-std/Vm.sol"; import {Test} from "@forge-std/Test.sol"; import {CoreV2} from "@voltprotocol/core/CoreV2.sol"; import {stdError} from "@forge-std/StdError.sol"; @@ -61,6 +60,7 @@ contract UnitTestMorphoCompoundPCVDeposit is Test { address(core), address(morpho), address(token), + address(0), address(morpho), address(morpho) ); @@ -83,7 +83,7 @@ contract UnitTestMorphoCompoundPCVDeposit 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"); @@ -98,19 +98,6 @@ contract UnitTestMorphoCompoundPCVDeposit is Test { assertEq(morphoDeposit.lastRecordedBalance(), 0); } - function testUnderlyingMismatchConstructionFails() public { - MockCToken cToken = new MockCToken(address(1)); - - vm.expectRevert("MorphoCompoundPCVDeposit: Underlying mismatch"); - new MorphoCompoundPCVDeposit( - address(core), - address(cToken), - address(token), - address(morpho), - address(morpho) - ); - } - function testDeposit(uint120 depositAmount) public { assertEq(morphoDeposit.lastRecordedBalance(), 0); token.mint(address(morphoDeposit), depositAmount); @@ -348,6 +335,7 @@ contract UnitTestMorphoCompoundPCVDeposit is Test { address(core), address(maliciousMorpho), /// cToken is not used in mock morpho deposit address(token), + address(0), address(maliciousMorpho), address(maliciousMorpho) ); diff --git a/test/unit/pcv/utils/ERC20Allocator.t.sol b/test/unit/pcv/utils/ERC20Allocator.t.sol index 390a23b2..480394ac 100644 --- a/test/unit/pcv/utils/ERC20Allocator.t.sol +++ b/test/unit/pcv/utils/ERC20Allocator.t.sol @@ -257,10 +257,7 @@ contract UnitTestERC20Allocator is Test { 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)); @@ -309,10 +306,7 @@ contract UnitTestERC20Allocator is Test { 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)); @@ -700,17 +694,14 @@ contract UnitTestERC20Allocator is Test { ( 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 @@ -729,17 +720,14 @@ contract UnitTestERC20Allocator is Test { ( 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); @@ -880,7 +868,7 @@ contract UnitTestERC20Allocator is Test { 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))); diff --git a/test/unit/system/System.t.sol b/test/unit/system/System.t.sol index ab406846..e4a231e6 100644 --- a/test/unit/system/System.t.sol +++ b/test/unit/system/System.t.sol @@ -615,6 +615,7 @@ contract SystemUnitTest is Test { address(core), address(mock), address(usdc), + address(0), address(mock), address(mock) );