Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Meta morpho scripts #1098

Merged
merged 12 commits into from
Mar 25, 2024
14 changes: 14 additions & 0 deletions common/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ export interface ITokens {
sUSDT?: string
sETH?: string
MORPHO?: string
SWISE?: string
BTRFLY?: string
astETH?: string
wsgUSDC?: string
wsgUSDbC?: string
Expand All @@ -87,6 +89,12 @@ export interface ITokens {
maWBTC?: string
maWETH?: string
maStETH?: string

// MetaMorpho
steakUSDC?: string
bbUSDT?: string
steakPYUSD?: string
Re7WETH?: string
}

export interface IFeeds {
Expand Down Expand Up @@ -202,11 +210,17 @@ export const networkConfig: { [key: string]: INetworkConfig } = {
sETH: '0x101816545F6bd2b1076434B54383a1E633390A2E',
astETH: '0x1982b2F5814301d4e9a8b0201555376e62F82428',
MORPHO: '0x9994e35db50125e0df82e4c2dde62496ce330999',
SWISE: '0x48C3399719B582dD63eB5AADf12A40B4C3f52FA2',
BTRFLY: '0xc55126051B22eBb829D00368f4B12Bde432de5Da',
yvCurveUSDPcrvUSD: '0xF56fB6cc29F0666BDD1662FEaAE2A3C935ee3469',
yvCurveUSDCcrvUSD: '0x7cA00559B978CFde81297849be6151d3ccB408A9',
pyUSD: '0x6c3ea9036406852006290770bedfcaba0e23a0e8',
aEthPyUSD: '0x0C0d01AbF3e6aDfcA0989eBbA9d6e85dD58EaB1E',
saEthPyUSD: '0x00F2a835758B33f3aC53516Ebd69f3dc77B0D152', // canonical wrapper
steakUSDC: '0xBEEF01735c132Ada46AA9aA4c54623cAA92A64CB',
steakPYUSD: '0xbEEF02e5E13584ab96848af90261f0C8Ee04722a',
bbUSDT: '0x2C25f6C25770fFEC5959D34B94Bf898865e5D6b1',
Re7WETH: '0x78Fc2c2eD1A4cDb5402365934aE5648aDAd094d0',
},
chainlinkFeeds: {
RSR: '0x759bBC1be8F90eE6457C44abc7d443842a976d02',
Expand Down
41 changes: 41 additions & 0 deletions contracts/plugins/assets/ERC4626FiatCollateral.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// SPDX-License-Identifier: BlueOak-1.0.0
pragma solidity 0.8.19;

// solhint-disable-next-line max-line-length
import { Asset, AppreciatingFiatCollateral, CollateralConfig, IRewardable } from "./AppreciatingFiatCollateral.sol";
import { OracleLib } from "./OracleLib.sol";
// solhint-disable-next-line max-line-length
import { AggregatorV3Interface } from "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
import { IERC4626 } from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import { shiftl_toFix } from "../../libraries/Fixed.sol";

/**
* @title ERC4626FiatCollateral
* @notice Collateral plugin for a ERC4626 vault
*
* Warning: Only valid for linear ERC4626 vaults
*/
contract ERC4626FiatCollateral is AppreciatingFiatCollateral {
uint256 private immutable oneShare;
int8 private immutable refDecimals;

/// config.erc20 must be a MetaMorpho 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)
AppreciatingFiatCollateral(config, revenueHiding)
{
require(address(config.erc20) != address(0), "missing erc20");
// require(config.defaultThreshold > 0, "defaultThreshold zero");
IERC4626 vault = IERC4626(address(config.erc20));
oneShare = 10**vault.decimals();
refDecimals = int8(uint8(IERC20Metadata(vault.asset()).decimals()));
}

/// @return {ref/tok} Actual quantity of whole reference units per whole collateral tokens
function underlyingRefPerTok() public view override returns (uint192) {
// already accounts for fees to be taken out
return shiftl_toFix(IERC4626(address(erc20)).convertToAssets(oneShare), -refDecimals);
}
}
23 changes: 23 additions & 0 deletions contracts/plugins/assets/meta-morpho/MetaMorphoFiatCollateral.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// SPDX-License-Identifier: BlueOak-1.0.0
pragma solidity 0.8.19;

import { CollateralConfig } from "../AppreciatingFiatCollateral.sol";
import { ERC4626FiatCollateral } from "../ERC4626FiatCollateral.sol";

/**
* @title MetaMorphoFiatCollateral
* @notice Collateral plugin for a MetaMorpho vault with fiat collateral, like USDC or USDT
* Expected: {tok} != {ref}, {ref} is pegged to {target} unless defaulting, {target} == {UoA}
*
* For example: steakUSDC, steakPYUSD, bbUSDT
*/
contract MetaMorphoFiatCollateral is ERC4626FiatCollateral {
/// config.erc20 must be a MetaMorpho 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)
ERC4626FiatCollateral(config, revenueHiding)
{
require(config.defaultThreshold > 0, "defaultThreshold zero");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// SPDX-License-Identifier: BlueOak-1.0.0
pragma solidity 0.8.19;

// solhint-disable-next-line max-line-length
import { AggregatorV3Interface } from "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
import { CollateralConfig } from "../AppreciatingFiatCollateral.sol";
import { FixLib, CEIL } from "../../../libraries/Fixed.sol";
import { OracleLib } from "../OracleLib.sol";
import { ERC4626FiatCollateral } from "../ERC4626FiatCollateral.sol";

/**
* @title MetaMorphoSelfReferentialCollateral
* @notice Collateral plugin for a MetaMorpho vault with self referential collateral, like WETH
* Expected: {tok} == {ref}, {ref} == {target}, {target} != {UoA}
*
* For example: Re7WETH
*/
contract MetaMorphoSelfReferentialCollateral is ERC4626FiatCollateral {
using FixLib for uint192;
using OracleLib for AggregatorV3Interface;

/// config.erc20 must be a MetaMorpho 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)
ERC4626FiatCollateral(config, revenueHiding)
{

Check warning on line 27 in contracts/plugins/assets/meta-morpho/MetaMorphoSelfReferentialCollateral.sol

View workflow job for this annotation

GitHub Actions / Lint Checks

Code contains empty blocks
// require(config.defaultThreshold > 0, "defaultThreshold zero");
}

/// Can revert, used by other contract functions in order to catch errors
/// @return low {UoA/tok} The low price estimate
/// @return high {UoA/tok} The high price estimate
/// @return pegPrice {target/ref}
function tryPrice()
external
view
override
returns (
uint192 low,
uint192 high,
uint192 pegPrice
)
{
// {UoA/tok} = {UoA/ref} * {ref/tok}
uint192 p = chainlinkFeed.price(oracleTimeout).mul(underlyingRefPerTok());
uint192 err = p.mul(oracleError, CEIL);

low = p - err;
high = p + err;
// assert(low <= high); obviously true just by inspection

pegPrice = targetPerRef();
}
}
27 changes: 27 additions & 0 deletions contracts/plugins/assets/meta-morpho/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# MetaMorpho

Morpho Blue is a permisionless lending protocol. At the time of this writing (March 19th, 2024), the only way to deposit is through something called **MetaMorpho**: (somewhat) managed ERC4626 vaults. Our integration with these tokens is straightforward with the exception of reward claiming, which occurs via supplying a merkle proof. This can be done permisionlessly and without interacting with any of our contracts, so any interaction with rewards is omitted here. The expectation is -- _and this is important to emphasize_ -- **any MORPHO reward claiming is left up to the RToken community to cause**.

## Up-only-ness

MetaMorpho suffers from a similar to that of the Curve volatile pools which can lose assets on admin fee claim.

## Target tokens

**USD**
| Name | Symbol | Address | Reward Tokens |
| -- | -- | -- | -- |
| Steakhouse USDC | steakUSDC| 0xBEEF01735c132Ada46AA9aA4c54623cAA92A64CB | wstETH, MORPHO |
| Steakhouse PYSUD | steakPYUSD | 0xbEEF02e5E13584ab96848af90261f0C8Ee04722a | MORPHO |
| Flagship USDT | bbUSDT| 0x2C25f6C25770fFEC5959D34B94Bf898865e5D6b1 | MORPHO |

**ETH**

| Name | Symbol | Address | Reward Tokens |
| -------- | ------- | ------------------------------------------ | --------------------------- |
| Re7 WETH | Re7WETH | 0x78Fc2c2eD1A4cDb5402365934aE5648aDAd094d0 | USDC, SWISE, BTRFLY, MORPHO |

## Future Work

- Assets need to exist for each of the Reward Tokens, which requires oracles. Only USDC meets this bar; SWISE, BTRFLY, and MORPHO do not have oracles yet.
- The right reward token assets need to be registered for an RToken as a function of their collateral. This can be done using the above table.
47 changes: 47 additions & 0 deletions contracts/plugins/mocks/MockMetaMorpho4626.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// SPDX-License-Identifier: BlueOak-1.0.0
pragma solidity 0.8.19;

import { IERC4626 } from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
import "@openzeppelin/contracts/utils/Address.sol";
import "../../libraries/Fixed.sol";

// Simple pass-through wrapper for real MetaMorpho ERC4626 vaults
// Allows settable asset count for testing
contract MockMetaMorpho4626 {
using FixLib for uint192;

IERC4626 public immutable actual; // the real ERC4626 vault

uint192 public multiplier = FIX_ONE;

// solhint-disable-next-line no-empty-blocks
constructor(IERC4626 _actual) {
actual = _actual;
}

function applyMultiple(uint192 multiple) external {
multiplier = multiplier.mul(multiple);
}

// === Pass-throughs ===

function balanceOf(address account) external view returns (uint256) {
return actual.balanceOf(account);
}

function asset() external view returns (address) {
return actual.asset();
}

function decimals() external view returns (uint8) {
return actual.decimals();
}

function convertToAssets(uint256 amount) external view returns (uint256) {
return multiplier.mulu_toUint(actual.convertToAssets(amount), CEIL);
}

function totalAssets() public view returns (uint256) {
return multiplier.mulu_toUint(actual.totalAssets(), CEIL);
}
}
6 changes: 5 additions & 1 deletion scripts/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,11 @@ async function main() {
'phase2-assets/collaterals/deploy_yearn_v2_curve_usdc.ts',
'phase2-assets/collaterals/deploy_yearn_v2_curve_usdp.ts',
'phase2-assets/collaterals/deploy_sfrax.ts',
'phase2-assets/collaterals/deploy_sfrax_eth.ts'
'phase2-assets/collaterals/deploy_sfrax_eth.ts',
'phase2-assets/collaterals/deploy_steakusdc.ts',
'phase2-assets/collaterals/deploy_steakpyusd.ts',
'phase2-assets/collaterals/deploy_bbusdt.ts',
'phase2-assets/collaterals/deploy_re7weth.ts'
)
} else if (chainId == '8453' || chainId == '84531') {
// Base L2 chains
Expand Down
91 changes: 91 additions & 0 deletions scripts/deployment/phase2-assets/collaterals/deploy_bbusdt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import fs from 'fs'
import hre from 'hardhat'
import { getChainId } from '../../../../common/blockchain-utils'
import { networkConfig } from '../../../../common/configuration'
import { fp } from '../../../../common/numbers'
import { expect } from 'chai'
import { CollateralStatus } from '../../../../common/constants'
import {
getDeploymentFile,
getAssetCollDeploymentFilename,
IAssetCollDeployments,
getDeploymentFilename,
fileExists,
} from '../../common'
import {
USDT_ORACLE_TIMEOUT,
USDT_ORACLE_ERROR,
USDT_USD_FEED,
PRICE_TIMEOUT,
DELAY_UNTIL_DEFAULT,
} from '../../../../test/plugins/individual-collateral/meta-morpho/constants'
import { MetaMorphoFiatCollateral } from '../../../../typechain'
import { ContractFactory } from 'ethers'

async function main() {
// ==== Read Configuration ====
const [deployer] = await hre.ethers.getSigners()

const chainId = await getChainId(hre)

console.log(`Deploying Collateral to network ${hre.network.name} (${chainId})
with burner account: ${deployer.address}`)

if (!networkConfig[chainId]) {
throw new Error(`Missing network configuration for ${hre.network.name}`)
}

// Get phase1 deployment
const phase1File = getDeploymentFilename(chainId)
if (!fileExists(phase1File)) {
throw new Error(`${phase1File} doesn't exist yet. Run phase 1`)
}
// Check previous step completed
const assetCollDeploymentFilename = getAssetCollDeploymentFilename(chainId)
const assetCollDeployments = <IAssetCollDeployments>getDeploymentFile(assetCollDeploymentFilename)

const deployedCollateral: string[] = []

/******** Deploy MetaMorpho Flagship USDT - bbUSDT **************************/

const MetaMorphoFiatCollateralFactory: ContractFactory = await hre.ethers.getContractFactory(
'MetaMorphoFiatCollateral'
)

const collateral = <MetaMorphoFiatCollateral>await MetaMorphoFiatCollateralFactory.connect(
deployer
).deploy(
{
priceTimeout: PRICE_TIMEOUT.toString(),
chainlinkFeed: USDT_USD_FEED,
oracleError: USDT_ORACLE_ERROR.toString(),
erc20: networkConfig[chainId].tokens.bbUSDT,
maxTradeVolume: fp('1e6').toString(), // $7.5m vault
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should be using much smaller numbers for this. The vaults are currently too small to sustain these.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I think $500k can make sense, but much lower and things become unusable on mainnet

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And <$500k for pyUSD, since it has $1.7m vault

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds good to me. Probably $250k for smaller vaults and $500k for every other.

oracleTimeout: USDT_ORACLE_TIMEOUT.toString(),
targetName: hre.ethers.utils.formatBytes32String('USD'),
defaultThreshold: USDT_ORACLE_ERROR.add(fp('0.01')).toString(), // +1% buffer rule
delayUntilDefault: DELAY_UNTIL_DEFAULT.toString(),
},
fp('1e-6') // small admin fee uncertainty
)
await collateral.deployed()

console.log(`Deployed bbUSDT to ${hre.network.name} (${chainId}): ${collateral.address}`)
await (await collateral.refresh()).wait()
expect(await collateral.status()).to.equal(CollateralStatus.SOUND)

assetCollDeployments.collateral.bbUSDT = collateral.address
assetCollDeployments.erc20s.bbUSDT = networkConfig[chainId].tokens.bbUSDT
deployedCollateral.push(collateral.address.toString())

fs.writeFileSync(assetCollDeploymentFilename, JSON.stringify(assetCollDeployments, null, 2))

console.log(`Deployed collateral to ${hre.network.name} (${chainId})
New deployments: ${deployedCollateral}
Deployment file: ${assetCollDeploymentFilename}`)
}

main().catch((error) => {
console.error(error)
process.exitCode = 1
})
Loading
Loading