diff --git a/script/DeployMoonwellViewsV3.s.sol b/script/DeployMoonwellViewsV3.s.sol index 2b9e02ebf..9a95d9ced 100644 --- a/script/DeployMoonwellViewsV3.s.sol +++ b/script/DeployMoonwellViewsV3.s.sol @@ -1,5 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.19; +pragma experimental ABIEncoderV2; import {console} from "@forge-std/console.sol"; import {Script} from "@forge-std/Script.sol"; @@ -32,11 +33,10 @@ contract DeployMoonwellViewsV3 is Script, Test { address unitroller = addresses.getAddress("UNITROLLER"); address tokenSaleDistributor = address(0); - address safetyModule = address(0); + address safetyModule = addresses.getAddress("stkWELL_PROXY"); address governanceToken = addresses.getAddress("xWELL_PROXY"); address nativeMarket = address(0); address governanceTokenLP = address(0); - MoonwellViewsV3 viewsContract = new MoonwellViewsV3(); bytes memory initdata = abi.encodeWithSignature( @@ -51,7 +51,7 @@ contract DeployMoonwellViewsV3 is Script, Test { ProxyAdmin proxyAdmin = new ProxyAdmin(); - new TransparentUpgradeableProxy( + TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy( address(viewsContract), address(proxyAdmin), initdata diff --git a/script/DeployMorphoViews.s.sol b/script/DeployMorphoViews.s.sol new file mode 100644 index 000000000..5401a9a0d --- /dev/null +++ b/script/DeployMorphoViews.s.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.19; +pragma experimental ABIEncoderV2; + +import {console} from "@forge-std/console.sol"; +import {Script} from "@forge-std/Script.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +import "@forge-std/Test.sol"; + +import {AllChainAddresses as Addresses} from "@proposals/Addresses.sol"; +import {MorphoViews} from "@protocol/views/MorphoViews.sol"; +import {TransparentUpgradeableProxy, ITransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; + +import {console} from "@forge-std/console.sol"; +/* +to run: +forge script script/DeployMorphoViews.s.sol:DeployMorphoViews -vvvv --rpc-url {rpc} --broadcast --etherscan-api-key {key} +forge script script/DeployMorphoViews.s.sol:DeployMorphoViews -vvvv --rpc-url https://sepolia.base.org --broadcast + +*/ + +contract DeployMorphoViews is Script, Test { + uint256 public PRIVATE_KEY; + + Addresses public addresses; + + function setUp() public { + addresses = new Addresses(); + + // Default behavior: use Anvil 0 private key + PRIVATE_KEY = vm.envOr( + "MOONWELL_DEPLOY_PK", + 77814517325470205911140941194401928579557062014761831930645393041380819009408 + ); + } + + function run() public { + vm.startBroadcast(PRIVATE_KEY); + + address unitroller = addresses.getAddress("UNITROLLER"); + address morpho = 0xce95AfbB8EA029495c66020883F87aaE8864AF92; + + MorphoViews viewsContract = new MorphoViews(); + + bytes memory initdata = abi.encodeWithSignature( + "initialize(address,address)", + unitroller, + morpho + ); + + ProxyAdmin proxyAdmin = new ProxyAdmin(); + + new TransparentUpgradeableProxy( + address(viewsContract), + address(proxyAdmin), + initdata + ); + + vm.stopBroadcast(); + } +} diff --git a/src/views/BaseMoonwellViews.sol b/src/views/BaseMoonwellViews.sol index 9d9d82436..22e262da3 100644 --- a/src/views/BaseMoonwellViews.sol +++ b/src/views/BaseMoonwellViews.sol @@ -10,6 +10,8 @@ import {Well} from "@protocol/governance/Well.sol"; import {IERC20} from "@protocol/governance/IERC20.sol"; import {MErc20Interface} from "@protocol/MTokenInterfaces.sol"; import {UniswapV2PairInterface} from "@protocol/views/UniswapV2PairInterface.sol"; +import {AggregatorV3Interface} from "@protocol/oracles/AggregatorV3Interface.sol"; +import {ChainlinkOracle} from "@protocol/oracles/ChainlinkOracle.sol"; /** * @title Moonwell Views Contract diff --git a/src/views/MetaMorphoInterface.sol b/src/views/MetaMorphoInterface.sol new file mode 100644 index 000000000..3d4d9477b --- /dev/null +++ b/src/views/MetaMorphoInterface.sol @@ -0,0 +1,252 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.5.0; + +import {IMorpho, Id, MarketParams} from "./MorphoBlueInterface.sol"; +import {IERC4626} from "../../lib/openzeppelin-contracts/contracts/interfaces/IERC4626.sol"; +import {IERC20Permit} from "../../lib/openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Permit.sol"; + +struct MarketConfig { + /// @notice The maximum amount of assets that can be allocated to the market. + uint184 cap; + /// @notice Whether the market is in the withdraw queue. + bool enabled; + /// @notice The timestamp at which the market can be instantly removed from the withdraw queue. + uint64 removableAt; +} + +struct PendingUint192 { + /// @notice The pending value to set. + uint192 value; + /// @notice The timestamp at which the pending value becomes valid. + uint64 validAt; +} + +struct PendingAddress { + /// @notice The pending value to set. + address value; + /// @notice The timestamp at which the pending value becomes valid. + uint64 validAt; +} + +struct MarketAllocation { + /// @notice The market to allocate. + MarketParams marketParams; + /// @notice The amount of assets to allocate. + uint256 assets; +} + +interface IMulticall { + function multicall(bytes[] calldata) external returns (bytes[] memory); +} + +interface IOwnable { + function owner() external view returns (address); + function transferOwnership(address) external; + function renounceOwnership() external; + function acceptOwnership() external; + function pendingOwner() external view returns (address); +} + +/// @dev This interface is used for factorizing IMetaMorphoStaticTyping and IMetaMorpho. +/// @dev Consider using the IMetaMorpho interface instead of this one. +interface IMetaMorphoBase { + /// @notice The address of the Morpho contract. + function MORPHO() external view returns (IMorpho); + function DECIMALS_OFFSET() external view returns (uint8); + + /// @notice The address of the curator. + function curator() external view returns (address); + + /// @notice Stores whether an address is an allocator or not. + function isAllocator(address target) external view returns (bool); + + /// @notice The current guardian. Can be set even without the timelock set. + function guardian() external view returns (address); + + /// @notice The current fee. + function fee() external view returns (uint96); + + /// @notice The fee recipient. + function feeRecipient() external view returns (address); + + /// @notice The skim recipient. + function skimRecipient() external view returns (address); + + /// @notice The current timelock. + function timelock() external view returns (uint256); + + /// @dev Stores the order of markets on which liquidity is supplied upon deposit. + /// @dev Can contain any market. A market is skipped as soon as its supply cap is reached. + function supplyQueue(uint256) external view returns (Id); + + /// @notice Returns the length of the supply queue. + function supplyQueueLength() external view returns (uint256); + + /// @dev Stores the order of markets from which liquidity is withdrawn upon withdrawal. + /// @dev Always contain all non-zero cap markets as well as all markets on which the vault supplies liquidity, + /// without duplicate. + function withdrawQueue(uint256) external view returns (Id); + + /// @notice Returns the length of the withdraw queue. + function withdrawQueueLength() external view returns (uint256); + + /// @notice Stores the total assets managed by this vault when the fee was last accrued. + /// @dev May be greater than `totalAssets()` due to removal of markets with non-zero supply or socialized bad debt. + /// This difference will decrease the fee accrued until one of the functions updating `lastTotalAssets` is + /// triggered (deposit/mint/withdraw/redeem/setFee/setFeeRecipient). + function lastTotalAssets() external view returns (uint256); + + /// @notice Submits a `newTimelock`. + /// @dev Warning: Reverts if a timelock is already pending. Revoke the pending timelock to overwrite it. + /// @dev In case the new timelock is higher than the current one, the timelock is set immediately. + function submitTimelock(uint256 newTimelock) external; + + /// @notice Accepts the pending timelock. + function acceptTimelock() external; + + /// @notice Revokes the pending timelock. + /// @dev Does not revert if there is no pending timelock. + function revokePendingTimelock() external; + + /// @notice Submits a `newSupplyCap` for the market defined by `marketParams`. + /// @dev Warning: Reverts if a cap is already pending. Revoke the pending cap to overwrite it. + /// @dev Warning: Reverts if a market removal is pending. + /// @dev In case the new cap is lower than the current one, the cap is set immediately. + function submitCap( + MarketParams memory marketParams, + uint256 newSupplyCap + ) external; + + /// @notice Accepts the pending cap of the market defined by `marketParams`. + function acceptCap(MarketParams memory marketParams) external; + + /// @notice Revokes the pending cap of the market defined by `id`. + /// @dev Does not revert if there is no pending cap. + function revokePendingCap(Id id) external; + + /// @notice Submits a forced market removal from the vault, eventually losing all funds supplied to the market. + /// @notice Funds can be recovered by enabling this market again and withdrawing from it (using `reallocate`), + /// but funds will be distributed pro-rata to the shares at the time of withdrawal, not at the time of removal. + /// @notice This forced removal is expected to be used as an emergency process in case a market constantly reverts. + /// To softly remove a sane market, the curator role is expected to bundle a reallocation that empties the market + /// first (using `reallocate`), followed by the removal of the market (using `updateWithdrawQueue`). + /// @dev Warning: Removing a market with non-zero supply will instantly impact the vault's price per share. + /// @dev Warning: Reverts for non-zero cap or if there is a pending cap. Successfully submitting a zero cap will + /// prevent such reverts. + function submitMarketRemoval(MarketParams memory marketParams) external; + + /// @notice Revokes the pending removal of the market defined by `id`. + /// @dev Does not revert if there is no pending market removal. + function revokePendingMarketRemoval(Id id) external; + + /// @notice Submits a `newGuardian`. + /// @notice Warning: a malicious guardian could disrupt the vault's operation, and would have the power to revoke + /// any pending guardian. + /// @dev In case there is no guardian, the gardian is set immediately. + /// @dev Warning: Submitting a gardian will overwrite the current pending gardian. + function submitGuardian(address newGuardian) external; + + /// @notice Accepts the pending guardian. + function acceptGuardian() external; + + /// @notice Revokes the pending guardian. + function revokePendingGuardian() external; + + /// @notice Skims the vault `token` balance to `skimRecipient`. + function skim(address) external; + + /// @notice Sets `newAllocator` as an allocator or not (`newIsAllocator`). + function setIsAllocator(address newAllocator, bool newIsAllocator) external; + + /// @notice Sets `curator` to `newCurator`. + function setCurator(address newCurator) external; + + /// @notice Sets the `fee` to `newFee`. + function setFee(uint256 newFee) external; + + /// @notice Sets `feeRecipient` to `newFeeRecipient`. + function setFeeRecipient(address newFeeRecipient) external; + + /// @notice Sets `skimRecipient` to `newSkimRecipient`. + function setSkimRecipient(address newSkimRecipient) external; + + /// @notice Sets `supplyQueue` to `newSupplyQueue`. + /// @param newSupplyQueue is an array of enabled markets, and can contain duplicate markets, but it would only + /// increase the cost of depositing to the vault. + function setSupplyQueue(Id[] calldata newSupplyQueue) external; + + /// @notice Updates the withdraw queue. Some markets can be removed, but no market can be added. + /// @notice Removing a market requires the vault to have 0 supply on it, or to have previously submitted a removal + /// for this market (with the function `submitMarketRemoval`). + /// @notice Warning: Anyone can supply on behalf of the vault so the call to `updateWithdrawQueue` that expects a + /// market to be empty can be griefed by a front-run. To circumvent this, the allocator can simply bundle a + /// reallocation that withdraws max from this market with a call to `updateWithdrawQueue`. + /// @dev Warning: Removing a market with supply will decrease the fee accrued until one of the functions updating + /// `lastTotalAssets` is triggered (deposit/mint/withdraw/redeem/setFee/setFeeRecipient). + /// @dev Warning: `updateWithdrawQueue` is not idempotent. Submitting twice the same tx will change the queue twice. + /// @param indexes The indexes of each market in the previous withdraw queue, in the new withdraw queue's order. + function updateWithdrawQueue(uint256[] calldata indexes) external; + + /// @notice Reallocates the vault's liquidity so as to reach a given allocation of assets on each given market. + /// @notice The allocator can withdraw from any market, even if it's not in the withdraw queue, as long as the loan + /// token of the market is the same as the vault's asset. + /// @dev The behavior of the reallocation can be altered by state changes, including: + /// - Deposits on the vault that supplies to markets that are expected to be supplied to during reallocation. + /// - Withdrawals from the vault that withdraws from markets that are expected to be withdrawn from during + /// reallocation. + /// - Donations to the vault on markets that are expected to be supplied to during reallocation. + /// - Withdrawals from markets that are expected to be withdrawn from during reallocation. + /// @dev Sender is expected to pass `assets = type(uint256).max` with the last MarketAllocation of `allocations` to + /// supply all the remaining withdrawn liquidity, which would ensure that `totalWithdrawn` = `totalSupplied`. + function reallocate(MarketAllocation[] calldata allocations) external; +} + +/// @dev This interface is inherited by MetaMorpho so that function signatures are checked by the compiler. +/// @dev Consider using the IMetaMorpho interface instead of this one. +interface IMetaMorphoStaticTyping is IMetaMorphoBase { + /// @notice Returns the current configuration of each market. + function config( + Id + ) external view returns (uint184 cap, bool enabled, uint64 removableAt); + + /// @notice Returns the pending guardian. + function pendingGuardian() + external + view + returns (address guardian, uint64 validAt); + + /// @notice Returns the pending cap for each market. + function pendingCap( + Id + ) external view returns (uint192 value, uint64 validAt); + + /// @notice Returns the pending timelock. + function pendingTimelock() + external + view + returns (uint192 value, uint64 validAt); +} + +/// @title IMetaMorpho +/// @author Morpho Labs +/// @custom:contact security@morpho.org +/// @dev Use this interface for MetaMorpho to have access to all the functions with the appropriate function signatures. +interface IMetaMorpho is + IMetaMorphoBase, + IERC4626, + IERC20Permit, + IOwnable, + IMulticall +{ + /// @notice Returns the current configuration of each market. + function config(Id) external view returns (MarketConfig memory); + + /// @notice Returns the pending guardian. + function pendingGuardian() external view returns (PendingAddress memory); + + /// @notice Returns the pending cap for each market. + function pendingCap(Id) external view returns (PendingUint192 memory); + + /// @notice Returns the pending timelock. + function pendingTimelock() external view returns (PendingUint192 memory); +} diff --git a/src/views/MorphoBlueInterface.sol b/src/views/MorphoBlueInterface.sol new file mode 100644 index 000000000..b5d540c69 --- /dev/null +++ b/src/views/MorphoBlueInterface.sol @@ -0,0 +1,427 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.5.0; + +type Id is bytes32; + +struct MarketParams { + address loanToken; + address collateralToken; + address oracle; + address irm; + uint256 lltv; +} + +/// @dev Warning: For `feeRecipient`, `supplyShares` does not contain the accrued shares since the last interest +/// accrual. +struct Position { + uint256 supplyShares; + uint128 borrowShares; + uint128 collateral; +} + +/// @dev Warning: `totalSupplyAssets` does not contain the accrued interest since the last interest accrual. +/// @dev Warning: `totalBorrowAssets` does not contain the accrued interest since the last interest accrual. +/// @dev Warning: `totalSupplyShares` does not contain the additional shares accrued by `feeRecipient` since the last +/// interest accrual. +struct Market { + uint128 totalSupplyAssets; + uint128 totalSupplyShares; + uint128 totalBorrowAssets; + uint128 totalBorrowShares; + uint128 lastUpdate; + uint128 fee; +} + +struct Authorization { + address authorizer; + address authorized; + bool isAuthorized; + uint256 nonce; + uint256 deadline; +} + +struct Signature { + uint8 v; + bytes32 r; + bytes32 s; +} + +interface IOracle { + /// @notice Returns the price of 1 asset of collateral token quoted in 1 asset of loan token, scaled by 1e36. + /// @dev It corresponds to the price of 10**(collateral token decimals) assets of collateral token quoted in + /// 10**(loan token decimals) assets of loan token with `36 + loan token decimals - collateral token decimals` + /// decimals of precision. + function price() external view returns (uint256); +} + +/// @title IIrm +/// @author Morpho Labs +/// @custom:contact security@morpho.org +/// @notice Interface that Interest Rate Models (IRMs) used by Morpho must implement. +interface IIrm { + /// @notice Returns the borrow rate per second (scaled by WAD) of the market `marketParams`. + /// @dev Assumes that `market` corresponds to `marketParams`. + function borrowRate( + MarketParams memory marketParams, + Market memory market + ) external returns (uint256); + + /// @notice Returns the borrow rate per second (scaled by WAD) of the market `marketParams` without modifying any + /// storage. + /// @dev Assumes that `market` corresponds to `marketParams`. + function borrowRateView( + MarketParams memory marketParams, + Market memory market + ) external view returns (uint256); +} + +/// @dev This interface is used for factorizing IMorphoStaticTyping and IMorpho. +/// @dev Consider using the IMorpho interface instead of this one. +interface IMorphoBase { + /// @notice The EIP-712 domain separator. + /// @dev Warning: Every EIP-712 signed message based on this domain separator can be reused on another chain sharing + /// the same chain id because the domain separator would be the same. + function DOMAIN_SEPARATOR() external view returns (bytes32); + + /// @notice The owner of the contract. + /// @dev It has the power to change the owner. + /// @dev It has the power to set fees on markets and set the fee recipient. + /// @dev It has the power to enable but not disable IRMs and LLTVs. + function owner() external view returns (address); + + /// @notice The fee recipient of all markets. + /// @dev The recipient receives the fees of a given market through a supply position on that market. + function feeRecipient() external view returns (address); + + /// @notice Whether the `irm` is enabled. + function isIrmEnabled(address irm) external view returns (bool); + + /// @notice Whether the `lltv` is enabled. + function isLltvEnabled(uint256 lltv) external view returns (bool); + + /// @notice Whether `authorized` is authorized to modify `authorizer`'s position on all markets. + /// @dev Anyone is authorized to modify their own positions, regardless of this variable. + function isAuthorized( + address authorizer, + address authorized + ) external view returns (bool); + + /// @notice The `authorizer`'s current nonce. Used to prevent replay attacks with EIP-712 signatures. + function nonce(address authorizer) external view returns (uint256); + + /// @notice Sets `newOwner` as `owner` of the contract. + /// @dev Warning: No two-step transfer ownership. + /// @dev Warning: The owner can be set to the zero address. + function setOwner(address newOwner) external; + + /// @notice Enables `irm` as a possible IRM for market creation. + /// @dev Warning: It is not possible to disable an IRM. + function enableIrm(address irm) external; + + /// @notice Enables `lltv` as a possible LLTV for market creation. + /// @dev Warning: It is not possible to disable a LLTV. + function enableLltv(uint256 lltv) external; + + /// @notice Sets the `newFee` for the given market `marketParams`. + /// @param newFee The new fee, scaled by WAD. + /// @dev Warning: The recipient can be the zero address. + function setFee(MarketParams memory marketParams, uint256 newFee) external; + + /// @notice Sets `newFeeRecipient` as `feeRecipient` of the fee. + /// @dev Warning: If the fee recipient is set to the zero address, fees will accrue there and will be lost. + /// @dev Modifying the fee recipient will allow the new recipient to claim any pending fees not yet accrued. To + /// ensure that the current recipient receives all due fees, accrue interest manually prior to making any changes. + function setFeeRecipient(address newFeeRecipient) external; + + /// @notice Creates the market `marketParams`. + /// @dev Here is the list of assumptions on the market's dependencies (tokens, IRM and oracle) that guarantees + /// Morpho behaves as expected: + /// - The token should be ERC-20 compliant, except that it can omit return values on `transfer` and `transferFrom`. + /// - The token balance of Morpho should only decrease on `transfer` and `transferFrom`. In particular, tokens with + /// burn functions are not supported. + /// - The token should not re-enter Morpho on `transfer` nor `transferFrom`. + /// - The token balance of the sender (resp. receiver) should decrease (resp. increase) by exactly the given amount + /// on `transfer` and `transferFrom`. In particular, tokens with fees on transfer are not supported. + /// - The IRM should not re-enter Morpho. + /// - The oracle should return a price with the correct scaling. + /// @dev Here is a list of properties on the market's dependencies that could break Morpho's liveness properties + /// (funds could get stuck): + /// - The token can revert on `transfer` and `transferFrom` for a reason other than an approval or balance issue. + /// - A very high amount of assets (~1e35) supplied or borrowed can make the computation of `toSharesUp` and + /// `toSharesDown` overflow. + /// - The IRM can revert on `borrowRate`. + /// - A very high borrow rate returned by the IRM can make the computation of `interest` in `_accrueInterest` + /// overflow. + /// - The oracle can revert on `price`. Note that this can be used to prevent `borrow`, `withdrawCollateral` and + /// `liquidate` from being used under certain market conditions. + /// - A very high price returned by the oracle can make the computation of `maxBorrow` in `_isHealthy` overflow, or + /// the computation of `assetsRepaid` in `liquidate` overflow. + /// @dev The borrow share price of a market with less than 1e4 assets borrowed can be decreased by manipulations, to + /// the point where `totalBorrowShares` is very large and borrowing overflows. + function createMarket(MarketParams memory marketParams) external; + + /// @notice Supplies `assets` or `shares` on behalf of `onBehalf`, optionally calling back the caller's + /// `onMorphoSupply` function with the given `data`. + /// @dev Either `assets` or `shares` should be zero. Most use cases should rely on `assets` as an input so the + /// caller is guaranteed to have `assets` tokens pulled from their balance, but the possibility to mint a specific + /// amount of shares is given for full compatibility and precision. + /// @dev Supplying a large amount can revert for overflow. + /// @dev Supplying an amount of shares may lead to supply more or fewer assets than expected due to slippage. + /// Consider using the `assets` parameter to avoid this. + /// @param marketParams The market to supply assets to. + /// @param assets The amount of assets to supply. + /// @param shares The amount of shares to mint. + /// @param onBehalf The address that will own the increased supply position. + /// @param data Arbitrary data to pass to the `onMorphoSupply` callback. Pass empty data if not needed. + /// @return assetsSupplied The amount of assets supplied. + /// @return sharesSupplied The amount of shares minted. + function supply( + MarketParams memory marketParams, + uint256 assets, + uint256 shares, + address onBehalf, + bytes memory data + ) external returns (uint256 assetsSupplied, uint256 sharesSupplied); + + /// @notice Withdraws `assets` or `shares` on behalf of `onBehalf` and sends the assets to `receiver`. + /// @dev Either `assets` or `shares` should be zero. To withdraw max, pass the `shares`'s balance of `onBehalf`. + /// @dev `msg.sender` must be authorized to manage `onBehalf`'s positions. + /// @dev Withdrawing an amount corresponding to more shares than supplied will revert for underflow. + /// @dev It is advised to use the `shares` input when withdrawing the full position to avoid reverts due to + /// conversion roundings between shares and assets. + /// @param marketParams The market to withdraw assets from. + /// @param assets The amount of assets to withdraw. + /// @param shares The amount of shares to burn. + /// @param onBehalf The address of the owner of the supply position. + /// @param receiver The address that will receive the withdrawn assets. + /// @return assetsWithdrawn The amount of assets withdrawn. + /// @return sharesWithdrawn The amount of shares burned. + function withdraw( + MarketParams memory marketParams, + uint256 assets, + uint256 shares, + address onBehalf, + address receiver + ) external returns (uint256 assetsWithdrawn, uint256 sharesWithdrawn); + + /// @notice Borrows `assets` or `shares` on behalf of `onBehalf` and sends the assets to `receiver`. + /// @dev Either `assets` or `shares` should be zero. Most use cases should rely on `assets` as an input so the + /// caller is guaranteed to borrow `assets` of tokens, but the possibility to mint a specific amount of shares is + /// given for full compatibility and precision. + /// @dev `msg.sender` must be authorized to manage `onBehalf`'s positions. + /// @dev Borrowing a large amount can revert for overflow. + /// @dev Borrowing an amount of shares may lead to borrow fewer assets than expected due to slippage. + /// Consider using the `assets` parameter to avoid this. + /// @param marketParams The market to borrow assets from. + /// @param assets The amount of assets to borrow. + /// @param shares The amount of shares to mint. + /// @param onBehalf The address that will own the increased borrow position. + /// @param receiver The address that will receive the borrowed assets. + /// @return assetsBorrowed The amount of assets borrowed. + /// @return sharesBorrowed The amount of shares minted. + function borrow( + MarketParams memory marketParams, + uint256 assets, + uint256 shares, + address onBehalf, + address receiver + ) external returns (uint256 assetsBorrowed, uint256 sharesBorrowed); + + /// @notice Repays `assets` or `shares` on behalf of `onBehalf`, optionally calling back the caller's + /// `onMorphoReplay` function with the given `data`. + /// @dev Either `assets` or `shares` should be zero. To repay max, pass the `shares`'s balance of `onBehalf`. + /// @dev Repaying an amount corresponding to more shares than borrowed will revert for underflow. + /// @dev It is advised to use the `shares` input when repaying the full position to avoid reverts due to conversion + /// roundings between shares and assets. + /// @dev An attacker can front-run a repay with a small repay making the transaction revert for underflow. + /// @param marketParams The market to repay assets to. + /// @param assets The amount of assets to repay. + /// @param shares The amount of shares to burn. + /// @param onBehalf The address of the owner of the debt position. + /// @param data Arbitrary data to pass to the `onMorphoRepay` callback. Pass empty data if not needed. + /// @return assetsRepaid The amount of assets repaid. + /// @return sharesRepaid The amount of shares burned. + function repay( + MarketParams memory marketParams, + uint256 assets, + uint256 shares, + address onBehalf, + bytes memory data + ) external returns (uint256 assetsRepaid, uint256 sharesRepaid); + + /// @notice Supplies `assets` of collateral on behalf of `onBehalf`, optionally calling back the caller's + /// `onMorphoSupplyCollateral` function with the given `data`. + /// @dev Interest are not accrued since it's not required and it saves gas. + /// @dev Supplying a large amount can revert for overflow. + /// @param marketParams The market to supply collateral to. + /// @param assets The amount of collateral to supply. + /// @param onBehalf The address that will own the increased collateral position. + /// @param data Arbitrary data to pass to the `onMorphoSupplyCollateral` callback. Pass empty data if not needed. + function supplyCollateral( + MarketParams memory marketParams, + uint256 assets, + address onBehalf, + bytes memory data + ) external; + + /// @notice Withdraws `assets` of collateral on behalf of `onBehalf` and sends the assets to `receiver`. + /// @dev `msg.sender` must be authorized to manage `onBehalf`'s positions. + /// @dev Withdrawing an amount corresponding to more collateral than supplied will revert for underflow. + /// @param marketParams The market to withdraw collateral from. + /// @param assets The amount of collateral to withdraw. + /// @param onBehalf The address of the owner of the collateral position. + /// @param receiver The address that will receive the collateral assets. + function withdrawCollateral( + MarketParams memory marketParams, + uint256 assets, + address onBehalf, + address receiver + ) external; + + /// @notice Liquidates the given `repaidShares` of debt asset or seize the given `seizedAssets` of collateral on the + /// given market `marketParams` of the given `borrower`'s position, optionally calling back the caller's + /// `onMorphoLiquidate` function with the given `data`. + /// @dev Either `seizedAssets` or `repaidShares` should be zero. + /// @dev Seizing more than the collateral balance will underflow and revert without any error message. + /// @dev Repaying more than the borrow balance will underflow and revert without any error message. + /// @dev An attacker can front-run a liquidation with a small repay making the transaction revert for underflow. + /// @param marketParams The market of the position. + /// @param borrower The owner of the position. + /// @param seizedAssets The amount of collateral to seize. + /// @param repaidShares The amount of shares to repay. + /// @param data Arbitrary data to pass to the `onMorphoLiquidate` callback. Pass empty data if not needed. + /// @return The amount of assets seized. + /// @return The amount of assets repaid. + function liquidate( + MarketParams memory marketParams, + address borrower, + uint256 seizedAssets, + uint256 repaidShares, + bytes memory data + ) external returns (uint256, uint256); + + /// @notice Executes a flash loan. + /// @dev Flash loans have access to the whole balance of the contract (the liquidity and deposited collateral of all + /// markets combined, plus donations). + /// @dev Warning: Not ERC-3156 compliant but compatibility is easily reached: + /// - `flashFee` is zero. + /// - `maxFlashLoan` is the token's balance of this contract. + /// - The receiver of `assets` is the caller. + /// @param token The token to flash loan. + /// @param assets The amount of assets to flash loan. + /// @param data Arbitrary data to pass to the `onMorphoFlashLoan` callback. + function flashLoan( + address token, + uint256 assets, + bytes calldata data + ) external; + + /// @notice Sets the authorization for `authorized` to manage `msg.sender`'s positions. + /// @param authorized The authorized address. + /// @param newIsAuthorized The new authorization status. + function setAuthorization( + address authorized, + bool newIsAuthorized + ) external; + + /// @notice Sets the authorization for `authorization.authorized` to manage `authorization.authorizer`'s positions. + /// @dev Warning: Reverts if the signature has already been submitted. + /// @dev The signature is malleable, but it has no impact on the security here. + /// @dev The nonce is passed as argument to be able to revert with a different error message. + /// @param authorization The `Authorization` struct. + /// @param signature The signature. + function setAuthorizationWithSig( + Authorization calldata authorization, + Signature calldata signature + ) external; + + /// @notice Accrues interest for the given market `marketParams`. + function accrueInterest(MarketParams memory marketParams) external; + + /// @notice Returns the data stored on the different `slots`. + function extSloads( + bytes32[] memory slots + ) external view returns (bytes32[] memory); +} + +/// @dev This interface is inherited by Morpho so that function signatures are checked by the compiler. +/// @dev Consider using the IMorpho interface instead of this one. +interface IMorphoStaticTyping is IMorphoBase { + /// @notice The state of the position of `user` on the market corresponding to `id`. + /// @dev Warning: For `feeRecipient`, `supplyShares` does not contain the accrued shares since the last interest + /// accrual. + function position( + Id id, + address user + ) + external + view + returns ( + uint256 supplyShares, + uint128 borrowShares, + uint128 collateral + ); + + /// @notice The state of the market corresponding to `id`. + /// @dev Warning: `totalSupplyAssets` does not contain the accrued interest since the last interest accrual. + /// @dev Warning: `totalBorrowAssets` does not contain the accrued interest since the last interest accrual. + /// @dev Warning: `totalSupplyShares` does not contain the accrued shares by `feeRecipient` since the last interest + /// accrual. + function market( + Id id + ) + external + view + returns ( + uint128 totalSupplyAssets, + uint128 totalSupplyShares, + uint128 totalBorrowAssets, + uint128 totalBorrowShares, + uint128 lastUpdate, + uint128 fee + ); + + /// @notice The market params corresponding to `id`. + /// @dev This mapping is not used in Morpho. It is there to enable reducing the cost associated to calldata on layer + /// 2s by creating a wrapper contract with functions that take `id` as input instead of `marketParams`. + function idToMarketParams( + Id id + ) + external + view + returns ( + address loanToken, + address collateralToken, + address oracle, + address irm, + uint256 lltv + ); +} + +/// @title IMorpho +/// @author Morpho Labs +/// @custom:contact security@morpho.org +/// @dev Use this interface for Morpho to have access to all the functions with the appropriate function signatures. +interface IMorpho is IMorphoBase { + /// @notice The state of the position of `user` on the market corresponding to `id`. + /// @dev Warning: For `feeRecipient`, `p.supplyShares` does not contain the accrued shares since the last interest + /// accrual. + function position( + Id id, + address user + ) external view returns (Position memory p); + + /// @notice The state of the market corresponding to `id`. + /// @dev Warning: `m.totalSupplyAssets` does not contain the accrued interest since the last interest accrual. + /// @dev Warning: `m.totalBorrowAssets` does not contain the accrued interest since the last interest accrual. + /// @dev Warning: `m.totalSupplyShares` does not contain the accrued shares by `feeRecipient` since the last + /// interest accrual. + function market(Id id) external view returns (Market memory m); + + /// @notice The market params corresponding to `id`. + /// @dev This mapping is not used in Morpho. It is there to enable reducing the cost associated to calldata on layer + /// 2s by creating a wrapper contract with functions that take `id` as input instead of `marketParams`. + function idToMarketParams( + Id id + ) external view returns (MarketParams memory); +} diff --git a/src/views/MorphoViews.sol b/src/views/MorphoViews.sol new file mode 100644 index 000000000..42450b85e --- /dev/null +++ b/src/views/MorphoViews.sol @@ -0,0 +1,658 @@ +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity 0.8.19; + +import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +import {Comptroller} from "@protocol/Comptroller.sol"; +import {MToken} from "@protocol/MToken.sol"; +import {TokenSaleDistributorInterfaceV1} from "@protocol/views/TokenSaleDistributorInterfaceV1.sol"; +import {SafetyModuleInterfaceV1} from "@protocol/views/SafetyModuleInterfaceV1.sol"; +import {Well} from "@protocol/governance/Well.sol"; +import {IERC20} from "@protocol/governance/IERC20.sol"; +import {MErc20Interface} from "@protocol/MTokenInterfaces.sol"; +import {UniswapV2PairInterface} from "@protocol/views/UniswapV2PairInterface.sol"; +import {IMorpho, IIrm, IOracle, MarketParams as MorphoMarketParams, Market as MorphoMarket, Id, Position as MorphoPosition} from "@protocol/views/MorphoBlueInterface.sol"; +import {IMetaMorpho} from "@protocol/views/MetaMorphoInterface.sol"; +import {AggregatorV3Interface} from "@protocol/oracles/AggregatorV3Interface.sol"; +import {ChainlinkOracle} from "@protocol/oracles/ChainlinkOracle.sol"; + +/** + * @title Moonwell Morpho Views Contract + * @author Moonwell + */ +contract MorphoViews is Initializable { + uint256 constant WAD = 1e18; + uint256 internal constant VIRTUAL_SHARES = 1e6; + uint256 internal constant VIRTUAL_ASSETS = 1; + uint256 internal constant POSITION_SLOT = 2; + uint256 internal constant SUPPLY_SHARES_OFFSET = 0; + uint256 internal constant BORROW_SHARES_AND_COLLATERAL_OFFSET = 1; + + struct UserMarketBalance { + Id marketId; + address collateralToken; + uint collateralAssets; + address loanToken; + uint loanAssets; + uint loanShares; + } + + struct MorphoBlueMarket { + Id marketId; + address collateralToken; + string collateralName; + string collateralSymbol; + uint collateralDecimals; + uint collateralPrice; + address loanToken; + string loanName; + string loanSymbol; + uint loanDecimals; + uint loanPrice; + uint totalSupplyAssets; + uint totalBorrowAssets; + uint totalLiquidity; + uint lltv; + uint supplyApy; + uint borrowApy; + uint fee; + address oracle; + uint256 oraclePrice; + address irm; + } + + struct MorphoVaultMarketsInfo { + Id marketId; + address marketCollateral; + string marketCollateralName; + string marketCollateralSymbol; + uint marketLiquidity; + uint marketLltv; + uint marketApy; + uint vaultAllocation; + uint vaultSupplied; + } + + struct MorphoVault { + address vault; + uint totalSupply; + uint totalAssets; + uint underlyingPrice; + uint fee; + uint timelock; + MorphoVaultMarketsInfo[] markets; + } + + Comptroller public comptroller; + IMorpho public morpho; + + /// construct the logic contract and initialize so that the initialize function is uncallable + /// from the implementation and only callable from the proxy + constructor() { + _disableInitializers(); + } + + function initialize( + address _comptroller, + address _morpho + ) external initializer { + // Sanity check the params + require( + _comptroller != address(0), + "Comptroller cant be the 0 address!" + ); + + comptroller = Comptroller(payable(_comptroller)); + + require( + comptroller.isComptroller(), + "Cant bind to something thats not a comptroller!" + ); + + morpho = IMorpho(_morpho); + } + + /// @dev Returns (`x` * `y`) / `WAD` rounded down. + function wMulDown(uint256 x, uint256 y) internal pure returns (uint256) { + return mulDivDown(x, y, WAD); + } + + /// @dev Returns (`x` * `WAD`) / `y` rounded down. + function wDivDown(uint256 x, uint256 y) internal pure returns (uint256) { + return mulDivDown(x, WAD, y); + } + + /// @dev Returns (`x` * `WAD`) / `y` rounded up. + function wDivUp(uint256 x, uint256 y) internal pure returns (uint256) { + return mulDivUp(x, WAD, y); + } + + /// @dev Returns (`x` * `y`) / `d` rounded down. + function mulDivDown( + uint256 x, + uint256 y, + uint256 d + ) internal pure returns (uint256) { + return (x * y) / d; + } + + /// @dev Returns (`x` * `y`) / `d` rounded up. + function mulDivUp( + uint256 x, + uint256 y, + uint256 d + ) internal pure returns (uint256) { + return (x * y + (d - 1)) / d; + } + + /// @dev Returns the sum of the first three non-zero terms of a Taylor expansion of e^(nx) - 1, to approximate a + /// continuous compound interest rate. + function wTaylorCompounded( + uint256 x, + uint256 n + ) internal pure returns (uint256) { + uint256 firstTerm = x * n; + uint256 secondTerm = mulDivDown(firstTerm, firstTerm, 2 * WAD); + uint256 thirdTerm = mulDivDown(secondTerm, firstTerm, 3 * WAD); + + return firstTerm + secondTerm + thirdTerm; + } + + /// @dev Returns `x` safely cast to uint128. + function toUint128(uint256 x) internal pure returns (uint128) { + require(x <= type(uint128).max, "MAX_UINT128_EXCEEDED"); + return uint128(x); + } + + /// @dev Calculates the value of `shares` quoted in assets, rounding up. + function toAssetsUp( + uint256 shares, + uint256 totalAssets, + uint256 totalShares + ) internal pure returns (uint256) { + return + mulDivUp( + shares, + totalAssets + VIRTUAL_ASSETS, + totalShares + VIRTUAL_SHARES + ); + } + + /// @dev Calculates the value of `assets` quoted in shares, rounding down. + function toSharesDown( + uint256 assets, + uint256 totalAssets, + uint256 totalShares + ) internal pure returns (uint256) { + return + mulDivDown( + assets, + totalShares + VIRTUAL_SHARES, + totalAssets + VIRTUAL_ASSETS + ); + } + + /// @dev Calculates the value of `shares` quoted in assets, rounding down. + function toAssetsDown( + uint256 shares, + uint256 totalAssets, + uint256 totalShares + ) internal pure returns (uint256) { + return + mulDivDown( + shares, + totalAssets + VIRTUAL_ASSETS, + totalShares + VIRTUAL_SHARES + ); + } + + /// @notice Returns the expected market balances of a market after having accrued interest. + /// @return The expected total supply assets. + /// @return The expected total supply shares. + /// @return The expected total borrow assets. + /// @return The expected total borrow shares. + function expectedMarketBalances( + MorphoMarket memory market, + MorphoMarketParams memory marketParams + ) internal view returns (uint256, uint256, uint256, uint256) { + uint256 elapsed = block.timestamp - market.lastUpdate; + + // Skipped if elapsed == 0 or totalBorrowAssets == 0 because interest would be null, or if irm == address(0). + if ( + elapsed != 0 && + market.totalBorrowAssets != 0 && + marketParams.irm != address(0) + ) { + uint256 borrowRate = IIrm(marketParams.irm).borrowRateView( + marketParams, + market + ); + uint256 interest = wMulDown( + market.totalBorrowAssets, + wTaylorCompounded(borrowRate, elapsed) + ); + market.totalBorrowAssets += toUint128(interest); + market.totalSupplyAssets += toUint128(interest); + + if (market.fee != 0) { + uint256 feeAmount = wMulDown(interest, market.fee); + // The fee amount is subtracted from the total supply in this calculation to compensate for the fact + // that total supply is already updated. + uint256 feeShares = toSharesDown( + feeAmount, + market.totalSupplyAssets - feeAmount, + market.totalSupplyShares + ); + market.totalSupplyShares += toUint128(feeShares); + } + } + + return ( + market.totalSupplyAssets, + market.totalSupplyShares, + market.totalBorrowAssets, + market.totalBorrowShares + ); + } + + function morphoBlueBorrowAPY( + MorphoMarketParams memory marketParams, + MorphoMarket memory market + ) public view returns (uint256 borrowApy) { + if (marketParams.irm != address(0)) { + borrowApy = wTaylorCompounded( + IIrm(marketParams.irm).borrowRateView(marketParams, market), + 365 days + ); + } + } + + function morphoBlueSupplyAPY( + MorphoMarketParams memory marketParams, + MorphoMarket memory market + ) public view returns (uint256 supplyApy) { + ( + uint256 totalSupplyAssets, + , + uint256 totalBorrowAssets, + + ) = expectedMarketBalances(market, marketParams); + + if (marketParams.irm != address(0)) { + uint256 utilization = totalBorrowAssets == 0 + ? 0 + : wDivUp(totalBorrowAssets, totalSupplyAssets); + supplyApy = wMulDown( + wMulDown( + morphoBlueBorrowAPY(marketParams, market), + 1 ether - market.fee + ), + utilization + ); + } + } + + function getChainlinkPrice( + AggregatorV3Interface feed + ) internal view returns (uint256) { + (, int256 answer, , uint256 updatedAt, ) = AggregatorV3Interface(feed) + .latestRoundData(); + require(answer > 0, "Chainlink price cannot be lower than 0"); + require(updatedAt != 0, "Round is in incompleted state"); + + // Chainlink USD-denominated feeds store answers at 8 decimals + uint256 decimalDelta = feed.decimals() > 18 ? 0 : 18 - feed.decimals(); + // Ensure that we don't multiply the result by 0 + if (decimalDelta > 0) { + return uint256(answer) * (10 ** decimalDelta); + } else { + return uint256(answer); + } + } + + /// @notice A view to get a specific market info + function getVaultMarketInfo( + Id _marketId, + IMorpho _morpho, + IMetaMorpho _vault + ) external view returns (MorphoVaultMarketsInfo memory) { + MorphoVaultMarketsInfo memory _market; + + MorphoMarketParams memory _marketParams = _morpho.idToMarketParams( + _marketId + ); + + MorphoMarket memory _marketState = _morpho.market(_marketId); + + MorphoPosition memory _position = _morpho.position( + _marketId, + address(_vault) + ); + + ( + uint totalSupplyAssets, + uint totalSupplyShares, + uint totalBorrowAssets, + + ) = expectedMarketBalances(_marketState, _marketParams); + + if ( + totalSupplyAssets != 0 && address(_marketParams.irm) != address(0) + ) { + uint256 borrowRate = IIrm(_marketParams.irm).borrowRateView( + _marketParams, + _marketState + ); + + uint borrowAPY = wTaylorCompounded(borrowRate, (3600 * 24 * 365)); + + uint utilization = wDivUp(totalBorrowAssets, totalSupplyAssets); + + _market.marketApy = wMulDown( + wMulDown(borrowAPY, WAD - _marketState.fee), + utilization + ); + } + + uint supplyAssetsUser = toAssetsDown( + _position.supplyShares, + totalSupplyAssets, + totalSupplyShares + ); + + _market.marketCollateral = _marketParams.collateralToken; + if (_market.marketCollateral != address(0)) { + _market.marketCollateralName = MToken(_marketParams.collateralToken) + .name(); + _market.marketCollateralSymbol = MToken( + _marketParams.collateralToken + ).symbol(); + } + _market.marketId = _marketId; + _market.marketLiquidity = (_marketParams.lltv > 0 && + totalSupplyAssets > totalBorrowAssets) + ? totalSupplyAssets - totalBorrowAssets + : 0; + _market.marketLltv = _marketParams.lltv; + _market.vaultSupplied = supplyAssetsUser; + + return _market; + } + + /// @notice A view to get a specific market info + function getVaultInfo( + IMetaMorpho _vault + ) external view returns (MorphoVault memory) { + MorphoVault memory _result; + + AggregatorV3Interface priceFeed = ChainlinkOracle( + address(comptroller.oracle()) + ).getFeed(MToken(_vault.asset()).symbol()); + + if (address(priceFeed) != address(0)) { + _result.underlyingPrice = getChainlinkPrice(priceFeed); + } + + _result.fee = _vault.fee(); + _result.timelock = _vault.timelock(); + _result.totalAssets = _vault.totalAssets(); + _result.totalSupply = _vault.totalSupply(); + _result.vault = address(_vault); + + _result.markets = new MorphoVaultMarketsInfo[]( + _vault.withdrawQueueLength() + ); + + for (uint index = 0; index < _result.markets.length; index++) { + Id _marketId = _vault.withdrawQueue(index); + _result.markets[index] = this.getVaultMarketInfo( + _marketId, + _vault.MORPHO(), + _vault + ); + } + + return _result; + } + + /// @notice A view to return vaults config + function getVaultsInfo( + address[] calldata morphoVaults + ) external view returns (MorphoVault[] memory) { + MorphoVault[] memory _result = new MorphoVault[](morphoVaults.length); + + for (uint256 index = 0; index < morphoVaults.length; index++) { + _result[index] = this.getVaultInfo( + IMetaMorpho(morphoVaults[index]) + ); + } + + return _result; + } + + /// @notice A view to get a specific market info + function getMorphoBlueMarketInfo( + Id _marketId + ) external view returns (MorphoBlueMarket memory) { + MorphoBlueMarket memory _result; + + MorphoMarketParams memory _marketParams = morpho.idToMarketParams( + _marketId + ); + + MorphoMarket memory _marketState = morpho.market(_marketId); + + _result.marketId = _marketId; + _result.loanToken = _marketParams.loanToken; + + _result.collateralToken = _marketParams.collateralToken; + _result.loanToken = _marketParams.loanToken; + + if (_result.collateralToken != address(0)) { + _result.collateralSymbol = MToken(_result.collateralToken).symbol(); + _result.collateralName = MToken(_result.collateralToken).name(); + _result.collateralDecimals = MToken(_result.collateralToken) + .decimals(); + + AggregatorV3Interface priceFeed = ChainlinkOracle( + address(comptroller.oracle()) + ).getFeed(_result.collateralSymbol); + + if (address(priceFeed) != address(0)) { + _result.collateralPrice = getChainlinkPrice(priceFeed); + } + } + + if (_result.loanToken != address(0)) { + _result.loanSymbol = MToken(_result.loanToken).symbol(); + _result.loanName = MToken(_result.loanToken).name(); + _result.loanDecimals = MToken(_result.loanToken).decimals(); + + AggregatorV3Interface priceFeed = ChainlinkOracle( + address(comptroller.oracle()) + ).getFeed(_result.loanSymbol); + + if (address(priceFeed) != address(0)) { + _result.loanPrice = getChainlinkPrice(priceFeed); + } + } + + _result.borrowApy = this.morphoBlueBorrowAPY( + _marketParams, + _marketState + ); + + _result.supplyApy = this.morphoBlueSupplyAPY( + _marketParams, + _marketState + ); + + ( + uint256 totalSupplyAssets, + , + uint256 totalBorrowAssets, + + ) = expectedMarketBalances(_marketState, _marketParams); + + _result.totalSupplyAssets = totalSupplyAssets; + _result.totalBorrowAssets = totalBorrowAssets; + + _result.totalLiquidity = totalSupplyAssets > totalBorrowAssets + ? totalSupplyAssets - totalBorrowAssets + : 0; + + _result.fee = _marketState.fee; + _result.irm = _marketParams.irm; + _result.lltv = _marketParams.lltv; + _result.oracle = _marketParams.oracle; + + if (_result.oracle != address(0)) { + _result.oraclePrice = IOracle(_result.oracle).price(); + } + + return _result; + } + + /// @notice A view to enumerate all market configs + function getMorphoBlueMarketsInfo( + Id[] calldata _marketIds + ) external view returns (MorphoBlueMarket[] memory) { + MorphoBlueMarket[] memory _result = new MorphoBlueMarket[]( + _marketIds.length + ); + + for (uint256 index = 0; index < _marketIds.length; index++) { + _result[index] = this.getMorphoBlueMarketInfo(_marketIds[index]); + } + + return _result; + } + + function _array(bytes32 x) private pure returns (bytes32[] memory) { + bytes32[] memory res = new bytes32[](1); + res[0] = x; + return res; + } + + function positionSupplySharesSlot( + Id id, + address user + ) internal pure returns (bytes32) { + return + bytes32( + uint256( + keccak256( + abi.encode( + user, + keccak256(abi.encode(id, POSITION_SLOT)) + ) + ) + ) + SUPPLY_SHARES_OFFSET + ); + } + + function positionBorrowSharesAndCollateralSlot( + Id id, + address user + ) internal pure returns (bytes32) { + return + bytes32( + uint256( + keccak256( + abi.encode( + user, + keccak256(abi.encode(id, POSITION_SLOT)) + ) + ) + ) + BORROW_SHARES_AND_COLLATERAL_OFFSET + ); + } + + function supplyShares(Id id, address user) internal view returns (uint256) { + bytes32[] memory slot = _array(positionSupplySharesSlot(id, user)); + return uint256(morpho.extSloads(slot)[0]); + } + + function borrowShares(Id id, address user) internal view returns (uint256) { + bytes32[] memory slot = _array( + positionBorrowSharesAndCollateralSlot(id, user) + ); + return uint128(uint256(morpho.extSloads(slot)[0])); + } + + function collateral(Id id, address user) internal view returns (uint256) { + bytes32[] memory slot = _array( + positionBorrowSharesAndCollateralSlot(id, user) + ); + return uint256(morpho.extSloads(slot)[0] >> 128); + } + + function expectedBorrowBalance( + Id id, + MorphoMarketParams memory marketParams, + MorphoMarket memory market, + address user + ) internal view returns (uint256, uint256) { + uint256 _borrowShares = borrowShares(id, user); + ( + , + , + uint256 totalBorrowAssets, + uint256 totalBorrowShares + ) = expectedMarketBalances(market, marketParams); + + return ( + _borrowShares, + toAssetsUp(_borrowShares, totalBorrowAssets, totalBorrowShares) + ); + } + + /// @notice A view to get a specific market info + function getMorphoBlueUserBalance( + Id _marketId, + address user + ) external view returns (UserMarketBalance memory) { + UserMarketBalance memory _result; + + MorphoMarketParams memory _marketParams = morpho.idToMarketParams( + _marketId + ); + + MorphoMarket memory _marketState = morpho.market(_marketId); + + _result.marketId = _marketId; + _result.collateralToken = _marketParams.collateralToken; + _result.collateralAssets = collateral(_marketId, user); + + (uint256 loanShares, uint256 loanAssets) = expectedBorrowBalance( + _marketId, + _marketParams, + _marketState, + user + ); + + _result.loanToken = _marketParams.loanToken; + _result.loanShares = loanShares; + _result.loanAssets = loanAssets; + + return _result; + } + + /// @notice A view to enumerate all market configs + function getMorphoBlueUserBalances( + Id[] calldata _marketIds, + address user + ) external view returns (UserMarketBalance[] memory) { + UserMarketBalance[] memory _result = new UserMarketBalance[]( + _marketIds.length + ); + + for (uint256 index = 0; index < _marketIds.length; index++) { + _result[index] = this.getMorphoBlueUserBalance( + _marketIds[index], + user + ); + } + + return _result; + } +} diff --git a/test/integration/views/MorphoViews.t.sol b/test/integration/views/MorphoViews.t.sol new file mode 100644 index 000000000..ff0303f87 --- /dev/null +++ b/test/integration/views/MorphoViews.t.sol @@ -0,0 +1,165 @@ +pragma solidity 0.8.19; + +import "@forge-std/Test.sol"; + +import {MorphoViews} from "@protocol/views/MorphoViews.sol"; +import {TransparentUpgradeableProxy, ITransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {IMorpho, IIrm, IOracle, MarketParams as MorphoMarketParams, Market as MorphoMarket, Id, Position as MorphoPosition} from "@protocol/views/MorphoBlueInterface.sol"; +import {Addresses} from "@proposals/Addresses.sol"; +import {PostProposalCheck} from "@test/integration/PostProposalCheck.sol"; +import "forge-std/console.sol"; + +contract MorphoViewsTest is Test, PostProposalCheck { + MorphoViews public viewsContract; + + address public user = 0xd7854FC91f16a58D67EC3644981160B6ca9C41B8; + address public proxyAdmin = address(1337); + + address public comptroller; + address morpho = 0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb; + + function setUp() public override { + super.setUp(); + + comptroller = addresses.getAddress("UNITROLLER"); + viewsContract = new MorphoViews(); + + bytes memory initdata = abi.encodeWithSignature( + "initialize(address,address)", + comptroller, + morpho + ); + + TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy( + address(viewsContract), + proxyAdmin, + initdata + ); + + /// wire proxy up + viewsContract = MorphoViews(address(proxy)); + vm.rollFork(16317213); + } + + function testVaultsProtocolInfo() public { + address[] memory morphoVaults = new address[](2); + morphoVaults[0] = 0xa0E430870c4604CcfC7B38Ca7845B1FF653D0ff1; + morphoVaults[1] = 0xc1256Ae5FF1cf2719D4937adb3bbCCab2E00A2Ca; + + MorphoViews.MorphoVault[] memory _vaultsInfo = viewsContract + .getVaultsInfo(morphoVaults); + + for (uint index = 0; index < _vaultsInfo.length; index++) { + MorphoViews.MorphoVault memory _vault = _vaultsInfo[index]; + console.log("Vault fee %s", _vault.fee); + console.log("Vault timelock %s", _vault.timelock); + console.log("Vault totalAssets %s", _vault.totalAssets); + console.log("Vault totalSupply %s", _vault.totalSupply); + console.log("Vault underlyingPrice %s", _vault.underlyingPrice); + console.log("Vault markets %s", _vault.markets.length); + + for (uint y = 0; y < _vault.markets.length; y++) { + MorphoViews.MorphoVaultMarketsInfo memory _market = _vault + .markets[index]; + console.log( + " Market marketCollateral %s", + _market.marketCollateral + ); + console.log( + " Market marketCollateralName %s", + _market.marketCollateralName + ); + console.log( + " Market marketCollateralSymbol %s", + _market.marketCollateralSymbol + ); + console.log(" Market apy %s", _market.marketApy); + console.log( + " Market marketLiquidity %s", + _market.marketLiquidity + ); + console.log(" Market marketLltv %s", _market.marketLltv); + console.log(" Market vaultSupplied %s", _market.vaultSupplied); + } + } + + assertEq(_vaultsInfo.length, 2); + } + + function testMorphoMarketsInfo() public { + Id[] memory markets = new Id[](1); + markets[0] = Id.wrap( + bytes32( + 0xdba352d93a64b17c71104cbddc6aef85cd432322a1446b5b65163cbbc615cd0c + ) + ); + + MorphoViews.MorphoBlueMarket[] memory _marketInfo = viewsContract + .getMorphoBlueMarketsInfo(markets); + + for (uint index = 0; index < markets.length; index++) { + MorphoViews.MorphoBlueMarket memory _market = _marketInfo[index]; + console.log( + "Market marketId %s", + string(abi.encodePacked(Id.unwrap(_market.marketId))) + ); + + console.log("Market collateralToken %s", _market.collateralToken); + console.log("Market collateralName %s", _market.collateralName); + console.log("Market collateralSymbol %s", _market.collateralSymbol); + console.log("Market collateralPrice %s", _market.collateralPrice); + + console.log("Market loanToken %s", _market.loanToken); + console.log("Market loanName %s", _market.loanName); + console.log("Market loanSymbol %s", _market.loanSymbol); + console.log("Market loanPrice %s", _market.loanPrice); + + console.log("Market fee %s", _market.fee); + console.log("Market irm %s", _market.irm); + console.log("Market lltv %s", _market.lltv); + console.log("Market oracle %s", _market.oracle); + console.log("Market oraclePrice %s", _market.oraclePrice); + + console.log( + "Market totalSupplyAssets %s", + _market.totalSupplyAssets + ); + console.log( + "Market totalBorrowAssets %s", + _market.totalBorrowAssets + ); + console.log("Market totalLiquidity %s", _market.totalLiquidity); + } + + assertEq(_marketInfo.length, 1); + } + + function testUserBalances() public { + Id[] memory markets = new Id[](1); + markets[0] = Id.wrap( + bytes32( + 0xdba352d93a64b17c71104cbddc6aef85cd432322a1446b5b65163cbbc615cd0c + ) + ); + + MorphoViews.UserMarketBalance[] memory _balances = viewsContract + .getMorphoBlueUserBalances(markets, user); + + for (uint index = 0; index < markets.length; index++) { + MorphoViews.UserMarketBalance memory _balance = _balances[index]; + console.log( + "User balances for marketId %s", + string(abi.encodePacked(Id.unwrap(_balance.marketId))) + ); + + console.log("User collateralToken %s", _balance.collateralToken); + console.log("User collateralAssets %s", _balance.collateralAssets); + + console.log("User loanToken %s", _balance.loanToken); + console.log("User loanAssets %s", _balance.loanAssets); + console.log("User loanShares %s", _balance.loanShares); + } + + assertEq(_balances.length, 1); + } +}