Skip to content
22 changes: 20 additions & 2 deletions common/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,16 @@ export interface ITokens {
Re7WETH?: string
meUSD?: string

// Morpho Vault V2
steakUSDCPrime?: string
sentoraPYUSD?: string
gauntletUSDCFrontier?: string
steakUSDTPrime?: string
galaxyUSDTQuality?: string
gauntletUSDCPrime?: string
galaxyUSDCQuality?: string
skyUSDTSavings?: string

pxETH?: string
apxETH?: string

Expand Down Expand Up @@ -313,7 +323,7 @@ export const networkConfig: { [key: string]: INetworkConfig } = {
sUSDT: '0x38EA452219524Bb87e18dE1C24D3bB59510BD783',
sETH: '0x101816545F6bd2b1076434B54383a1E633390A2E',
astETH: '0x1982b2F5814301d4e9a8b0201555376e62F82428',
MORPHO: '0x9994e35db50125e0df82e4c2dde62496ce330999',
MORPHO: '0x58D97B57BB95320F9a05dC918Aef65434969c2B2', // new, transferable MORPHO
SWISE: '0x48C3399719B582dD63eB5AADf12A40B4C3f52FA2',
BTRFLY: '0xc55126051B22eBb829D00368f4B12Bde432de5Da',
yvCurveUSDPcrvUSD: '0xF56fB6cc29F0666BDD1662FEaAE2A3C935ee3469',
Expand All @@ -325,6 +335,14 @@ export const networkConfig: { [key: string]: INetworkConfig } = {
steakPYUSD: '0xbEEF02e5E13584ab96848af90261f0C8Ee04722a',
bbUSDT: '0x2C25f6C25770fFEC5959D34B94Bf898865e5D6b1',
Re7WETH: '0x78Fc2c2eD1A4cDb5402365934aE5648aDAd094d0',
steakUSDCPrime: '0xbeef088055857739C12CD3765F20b7679Def0f51',
sentoraPYUSD: '0xb576765fB15505433aF24FEe2c0325895C559FB2', // PYUSD (Paypal USD Main)
gauntletUSDCFrontier: '0x9a1D6bd5b8642C41F25e0958129B85f8E1176F3e',
steakUSDTPrime: '0xbeef003C68896c7D2c3c60d363e8d71a49Ab2bf9',
galaxyUSDTQuality: '0x71ffB6a81786eC285D429d531Cf655107B9D878d',
gauntletUSDCPrime: '0x8c106EEDAd96553e64287A5A6839c3Cc78afA3D0',
galaxyUSDCQuality: '0x91600E31fBeDc72433d4a57F16639cfe661Be7d8',
skyUSDTSavings: '0x23f5E9c35820f4baB695Ac1F19c203cC3f8e1e11',
sdUSDCUSDCPlus: '0x9bbF31E99F30c38a5003952206C31EEa77540BeF',
USDe: '0x4c9edd5852cd905f086c759e8383e09bff1e68b3',
sUSDe: '0x9D39A5DE30e57443BfF2A8307A4256c8797A3497',
Expand Down Expand Up @@ -460,7 +478,7 @@ export const networkConfig: { [key: string]: INetworkConfig } = {
sUSDT: '0x38EA452219524Bb87e18dE1C24D3bB59510BD783',
sETH: '0x101816545F6bd2b1076434B54383a1E633390A2E',
astETH: '0x1982b2F5814301d4e9a8b0201555376e62F82428',
MORPHO: '0x9994e35db50125e0df82e4c2dde62496ce330999',
MORPHO: '0x58D97B57BB95320F9a05dC918Aef65434969c2B2', // new, transferable MORPHO
},
chainlinkFeeds: {
RSR: '0x759bBC1be8F90eE6457C44abc7d443842a976d02',
Expand Down
34 changes: 34 additions & 0 deletions contracts/plugins/assets/meta-morpho/IMorphoVaultV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// SPDX-License-Identifier: BlueOak-1.0.0
pragma solidity 0.8.28;

/**
* @title IMorphoVaultV2
* @notice Minimal interface for the gate + abdication surface of a Morpho Vault V2.
*
* Gates are optional external contracts that can restrict share transfers and asset flows.
* A gate setter that has been "abdicated" can never be called again, so a gate that is
* currently unset (`address(0)`) with an abdicated setter is permanently disabled.
*/
interface IMorphoVaultV2 {
// === Gate getters ===
function receiveSharesGate() external view returns (address);

function sendSharesGate() external view returns (address);

function receiveAssetsGate() external view returns (address);

function sendAssetsGate() external view returns (address);

/// @return Whether `selector` has been permanently abdicated (can no longer be called)
function abdicated(bytes4 selector) external view returns (bool);

// === Gate setters ===
// Declared only so their `.selector` can be referenced when checking abdication.
function setReceiveSharesGate(address newReceiveSharesGate) external;

function setSendSharesGate(address newSendSharesGate) external;

function setReceiveAssetsGate(address newReceiveAssetsGate) external;

function setSendAssetsGate(address newSendAssetsGate) external;
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { ERC4626FiatCollateral } from "../ERC4626FiatCollateral.sol";
*
* Rewards need to be claimed manually, from off-chain. This can be done permissionlessly,
* by anyone, on behalf of the RToken's Backing Manager address.
* For more information: https://docs.morpho.org/rewards/tutorials/claim-rewards/
* For more information: https://docs.morpho.org/learn/concepts/rewards/
*
*/
contract MetaMorphoFiatCollateral is ERC4626FiatCollateral {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { ERC4626FiatCollateral } from "../ERC4626FiatCollateral.sol";
*
* Rewards will need to be claimed manually, off-chain. This can be done permissionlessly by anyone,
* on behalf of the RToken's Backing Manager address.
* For more information: https://docs.morpho.org/rewards/tutorials/claim-rewards/
* For more information: https://docs.morpho.org/learn/concepts/rewards/
*
*/
contract MetaMorphoSelfReferentialCollateral is ERC4626FiatCollateral {
Expand Down
151 changes: 151 additions & 0 deletions contracts/plugins/assets/meta-morpho/MorphoAsset.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// SPDX-License-Identifier: BlueOak-1.0.0
pragma solidity 0.8.28;

// solhint-disable-next-line max-line-length
import { AggregatorV3Interface } from "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import { FixLib, CEIL, FLOOR, shiftl_toFix } from "../../../libraries/Fixed.sol";
import { Asset } from "../Asset.sol";
import { OracleLib } from "../OracleLib.sol";
import { IUniswapV3Pool } from "./vendor/IUniswapV3Pool.sol";
import { UniswapV3TwapLib } from "./vendor/UniswapV3TwapLib.sol";

/**
* @title MorphoAsset
* @notice Asset plugin for the MORPHO reward token, which has no {UoA} price feed on mainnet.
*
* tok = MORPHO
* UoA = USD
*
* MORPHO is earned as a reward by holding MetaMorpho / Morpho Vault V2 collateral. Rewards are
* claimed off-chain via a Merkle proof, permissionlessly, on behalf of the RToken's Backing
* Manager. This Asset exists only so the claimed MORPHO can be sold as revenue -- it is never
* used as backing.
*
* Pricing: {UoA/tok} = {UoA/quoteTok} * {quoteTok/tok}
* - {UoA/quoteTok} comes from `chainlinkFeed` (e.g. ETH/USD)
* - {quoteTok/tok} comes from an arithmetic-mean-tick TWAP over `uniswapV3Pool`
*
* WARNING -- operational requirements for a safe deployment:
* 1. The pool's `observationCardinality` MUST be large enough to retain `twapWindow` seconds of
* observations, or `observe()` reverts ("OLD") and this Asset becomes unpriced. Cardinality
* is grown permissionlessly via `increaseObservationCardinalityNext()` and never shrinks;
* the constructor probes the window once, but a pool that is traded in many consecutive
* blocks can still evict history later. Size cardinality with headroom.
* 2. A TWAP is only as manipulation-resistant as the pool is deep. `maxTradeVolume` -- not the
* oracle -- is the binding protection here: a manipulated-downwards price lowers the
* DutchTrade floor, so cap per-auction exposure accordingly.
*/
contract MorphoAsset is Asset {
using FixLib for uint192;
using OracleLib for AggregatorV3Interface;
using UniswapV3TwapLib for IUniswapV3Pool;

/// The Uniswap V3 pool consulted for {quoteTok/tok}
IUniswapV3Pool public immutable uniswapV3Pool;

/// The token the pool prices `erc20` against; `chainlinkFeed` must be {UoA/quoteToken}
address public immutable quoteToken;

/// {s} The TWAP window over which the mean tick is taken
uint32 public immutable twapWindow;

/// {qTok} One whole unit of `erc20`, used as the TWAP base amount
uint128 private immutable oneTok;

/// The negated decimals of `quoteToken`, for converting the TWAP quote to a Fix
int8 private immutable quoteTokenDecimals;

/// @param priceTimeout_ {s} The number of seconds over which savedHighPrice decays to 0
/// @param chainlinkFeed_ Feed units: {UoA/quoteToken} -- e.g. ETH/USD
/// @param oracleError_ {1} The % the oracle feed can be off by
/// @param erc20_ The MORPHO ERC20
/// @param maxTradeVolume_ {UoA} The max trade volume, in UoA
/// @param oracleTimeout_ {s} The number of seconds until the chainlinkFeed becomes invalid
/// @param uniswapV3Pool_ The Uniswap V3 pool holding the erc20/quoteToken pair
/// @param quoteToken_ The other token in the pool; must be the feed's base unit
/// @param twapWindow_ {s} The TWAP window; longer is more manipulation-resistant
constructor(
uint48 priceTimeout_,
AggregatorV3Interface chainlinkFeed_,
uint192 oracleError_,
IERC20Metadata erc20_,
uint192 maxTradeVolume_,
uint48 oracleTimeout_,
IUniswapV3Pool uniswapV3Pool_,
IERC20Metadata quoteToken_,
uint32 twapWindow_
) Asset(priceTimeout_, chainlinkFeed_, oracleError_, erc20_, maxTradeVolume_, oracleTimeout_) {
require(address(uniswapV3Pool_) != address(0), "missing pool");
require(address(quoteToken_) != address(0), "missing quoteToken");
require(address(quoteToken_) != address(erc20_), "quoteToken is erc20");
require(twapWindow_ != 0, "twapWindow zero");

// The pool must hold exactly the {erc20, quoteToken} pair, in either order
address token0 = uniswapV3Pool_.token0();
address token1 = uniswapV3Pool_.token1();
require(
(token0 == address(erc20_) && token1 == address(quoteToken_)) ||
(token1 == address(erc20_) && token0 == address(quoteToken_)),
"pool token mismatch"
);

uniswapV3Pool = uniswapV3Pool_;
quoteToken = address(quoteToken_);
twapWindow = twapWindow_;
oneTok = uint128(10**erc20_.decimals());
quoteTokenDecimals = int8(uint8(quoteToken_.decimals()));

// Fail closed if the pool cannot currently serve the requested window
uniswapV3Pool_.consult(twapWindow_);
}

/// Can revert, used by other contract functions in order to catch errors
/// Should not return FIX_MAX for low
/// Should only return FIX_MAX for high if low is 0
/// Should NOT be manipulable by MEV
/// @dev The third (unused) return value is only here for compatibility with Collateral
/// @return low {UoA/tok} The low price estimate
/// @return high {UoA/tok} The high price estimate
function tryPrice()
external
view
virtual
override
returns (
uint192 low,
uint192 high,
uint192
)
{
// {UoA/quoteTok}
uint192 quoteTokenPrice = chainlinkFeed.price(oracleTimeout);

// {quoteTok/tok}
uint192 quoteTokPerTok = _twapQuoteTokPerTok();

// {UoA/tok} = {UoA/quoteTok} * {quoteTok/tok}
uint192 p = quoteTokenPrice.mul(quoteTokPerTok);
uint192 err = p.mul(oracleError, CEIL);
// assert(low <= high); obviously true just by inspection
return (p - err, p + err, 0);
}

// === Private ===

/// @return {quoteTok/tok} The TWAP price of one whole erc20 in quoteToken
function _twapQuoteTokPerTok() private view returns (uint192) {
int24 meanTick = uniswapV3Pool.consult(twapWindow);

// {qQuoteTok} per one whole {tok}
uint256 quoteAmount = UniswapV3TwapLib.getQuoteAtTick(
meanTick,
oneTok,
address(erc20),
quoteToken
);

// {quoteTok/tok} = {qQuoteTok} shifted by the quote token's decimals
return shiftl_toFix(quoteAmount, -quoteTokenDecimals, FLOOR);
}
}
56 changes: 56 additions & 0 deletions contracts/plugins/assets/meta-morpho/MorphoV2FiatCollateral.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// SPDX-License-Identifier: BlueOak-1.0.0
pragma solidity 0.8.28;

import { CollateralConfig } from "../AppreciatingFiatCollateral.sol";
import { MetaMorphoFiatCollateral } from "./MetaMorphoFiatCollateral.sol";
import { IMorphoVaultV2 } from "./IMorphoVaultV2.sol";

/**
* @title MorphoV2FiatCollateral
* @notice Collateral plugin for a Morpho Vault V2 with fiat collateral, like USDC, USDT or PYUSD
* Expected: {tok} != {ref}, {ref} is pegged to {target} unless defaulting, {target} == {UoA}
*
* Pricing and default behavior are identical to {MetaMorphoFiatCollateral}. This plugin only
* adds a one-time constructor guard that the vault's critical gates are permanently disabled.
*
* Morpho Vault V2 can install "gates" that restrict share transfers and asset flows. A
* receive-shares, send-shares, or receive-assets gate could block the protocol (or any holder)
* from holding, trading, or exiting the collateral. The check is done in the constructor rather
* than refresh() because gate abdication is permanent: if a critical gate is unset and its setter
* is abdicated at construction, it can never be set for the life of the collateral, so a single
* check is sufficient and costs no runtime gas.
*
* sendAssetsGate is intentionally NOT required to be abdicated: it only gates future deposit/mint
* into the vault, never the transfer or exit of existing shares.
*
* Rewards need to be claimed manually, from off-chain. This can be done permissionlessly,
* by anyone, on behalf of the RToken's Backing Manager address.
* For more information: https://docs.morpho.org/learn/concepts/rewards/
*/
contract MorphoV2FiatCollateral is MetaMorphoFiatCollateral {
/// @param config.erc20 must be a Morpho Vault V2 ERC4626 vault
/// @param config.chainlinkFeed Feed units: {UoA/ref}
/// @param revenueHiding {1} A value like 1e-6 that represents the maximum refPerTok to hide
constructor(CollateralConfig memory config, uint192 revenueHiding)
MetaMorphoFiatCollateral(config, revenueHiding)
{
IMorphoVaultV2 vault = IMorphoVaultV2(address(config.erc20));

// Each critical gate must be permanently disabled: unset AND its setter abdicated.
require(
vault.receiveSharesGate() == address(0) &&
vault.abdicated(IMorphoVaultV2.setReceiveSharesGate.selector),
"receiveSharesGate not abdicated"
);
require(
vault.sendSharesGate() == address(0) &&
vault.abdicated(IMorphoVaultV2.setSendSharesGate.selector),
"sendSharesGate not abdicated"
);
require(
vault.receiveAssetsGate() == address(0) &&
vault.abdicated(IMorphoVaultV2.setReceiveAssetsGate.selector),
"receiveAssetsGate not abdicated"
);
}
}
Loading
Loading