Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 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 @@ -325,6 +335,14 @@ export const networkConfig: { [key: string]: INetworkConfig } = {
steakPYUSD: '0xbEEF02e5E13584ab96848af90261f0C8Ee04722a',
bbUSDT: '0x2C25f6C25770fFEC5959D34B94Bf898865e5D6b1',
Re7WETH: '0x78Fc2c2eD1A4cDb5402365934aE5648aDAd094d0',
steakUSDCPrime: '0xbeef088055857739C12CD3765F20b7679Def0f51',
sentoraPYUSD: '0xC21b08C16458202593D4D9B26b9984Ee67b38BbD',
gauntletUSDCFrontier: '0x9a1D6bd5b8642C41F25e0958129B85f8E1176F3e',
steakUSDTPrime: '0xbeef003C68896c7D2c3c60d363e8d71a49Ab2bf9',
galaxyUSDTQuality: '0x71ffB6a81786eC285D429d531Cf655107B9D878d',
gauntletUSDCPrime: '0x8c106EEDAd96553e64287A5A6839c3Cc78afA3D0',
galaxyUSDCQuality: '0x91600E31fBeDc72433d4a57F16639cfe661Be7d8',
skyUSDTSavings: '0x23f5E9c35820f4baB695Ac1F19c203cC3f8e1e11',
sdUSDCUSDCPlus: '0x9bbF31E99F30c38a5003952206C31EEa77540BeF',
USDe: '0x4c9edd5852cd905f086c759e8383e09bff1e68b3',
sUSDe: '0x9D39A5DE30e57443BfF2A8307A4256c8797A3497',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ import { ERC4626FiatCollateral } from "../ERC4626FiatCollateral.sol";
* by anyone, on behalf of the RToken's Backing Manager address.
* For more information: https://docs.morpho.org/rewards/tutorials/claim-rewards/
*
* @dev Morpho Vault V2 compatibility
* This plugin is reused as-is for Morpho Vault V2 vaults (e.g. Steakhouse Prime USDC,
* Gauntlet USDC Prime, Galaxy/Sentora/Sky vaults). V2 preserves the exact ERC-4626 surface
* this plugin relies on, and the V2-specific deviations were checked on-chain to be benign:
*
*/
contract MetaMorphoFiatCollateral is ERC4626FiatCollateral {
/// config.erc20 must be a MetaMorpho ERC4626 vault
Expand Down
26 changes: 26 additions & 0 deletions contracts/plugins/assets/meta-morpho/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,32 @@ It is important to note that in the case of Rtokens, rewards will need to be cla
| -------- | ------- | ------------------------------------------ | --------------------------- |
| Re7 WETH | Re7WETH | 0x78Fc2c2eD1A4cDb5402365934aE5648aDAd094d0 | USDC, SWISE, BTRFLY, MORPHO |

## Morpho Vault V2

The same `MetaMorphoFiatCollateral` / `MetaMorphoSelfReferentialCollateral` contracts are reused, unchanged, for [Morpho Vault V2](https://docs.morpho.org/learn/concepts/vault-v2/) vaults. V2 keeps the exact ERC-4626 surface these plugins depend on (`convertToAssets`, `asset`, `decimals`), so `underlyingRefPerTok()` is correct without modification. The V2-specific deviations were checked on-chain and found benign:

| Check | Result |
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **ERC-4626 / 2612** | Compliant. 18-decimal share over a 6-decimal asset; `convertToAssets` accounts for fees. |
| **Gates** | V2 adds optional transfer/deposit/withdraw gate contracts. A share-transfer gate would block the protocol from holding/trading the collateral. **Every target vault must have all four gates unset (`address(0)`).** |
| **`max*` quirk** | V2 `maxDeposit`/`maxMint`/`maxWithdraw`/`maxRedeem` always return 0. Harmless: the protocol holds and trades the share token and never calls `vault.redeem()`. |
| **Fees & losses** | Performance/management fees and adapter losses flow through `convertToAssets`. Routine fee dips are absorbed by `revenueHiding`; a genuine loss correctly DISABLES the collateral. |

Mainnet vaults validated against the live chain (all gates unset; fees 0 except Sentora PYUSD at 15% performance fee):

| Name | Symbol | Address | Asset |
| ---------------------- | ------------------- | -------------------------------------------- | ----- |
| Steakhouse Prime USDC | steakUSDC | `0xbeef088055857739C12CD3765F20b7679Def0f51` | USDC |
| Sentora PYUSD Main | senPYUSDPRIMEv2 | `0xC21b08C16458202593D4D9B26b9984Ee67b38BbD` | PYUSD |
| Gauntlet USDC Frontier | gtusdcf | `0x9a1D6bd5b8642C41F25e0958129B85f8E1176F3e` | USDC |
| Steakhouse Prime USDT | steakUSDT | `0xbeef003C68896c7D2c3c60d363e8d71a49Ab2bf9` | USDT |
| Galaxy USDT Quality | gUSDTq | `0x71ffB6a81786eC285D429d531Cf655107B9D878d` | USDT |
| Gauntlet USDC Prime | gtusdcp | `0x8c106EEDAd96553e64287A5A6839c3Cc78afA3D0` | USDC |
| Galaxy USDC Quality | gUSDCq | `0x91600E31fBeDc72433d4a57F16639cfe661Be7d8` | USDC |
| Sky.money USDT Savings | skyMoneyUsdtSavings | `0x23f5E9c35820f4baB695Ac1F19c203cC3f8e1e11` | USDT |

Reward claiming is unchanged from V1 (off-chain Merkle claim on behalf of the Backing Manager).

## 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.
Expand Down
1 change: 1 addition & 0 deletions scripts/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ async function main() {
'phase2-assets/collaterals/deploy_steakpyusd.ts',
'phase2-assets/collaterals/deploy_bbusdt.ts',
'phase2-assets/collaterals/deploy_re7weth.ts',
'phase2-assets/collaterals/deploy_morpho_v2_vaults.ts',
'phase2-assets/collaterals/deploy_ethx_collateral.ts',
'phase2-assets/collaterals/deploy_apxeth.ts',
'phase2-assets/collaterals/deploy_USDe.ts',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import fs from 'fs'
import hre from 'hardhat'
import { getChainId } from '../../../../common/blockchain-utils'
import { networkConfig, ITokens } 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 {
USDC_ORACLE_TIMEOUT,
USDC_ORACLE_ERROR,
USDC_USD_FEED,
USDT_ORACLE_TIMEOUT,
USDT_ORACLE_ERROR,
USDT_USD_FEED,
PYUSD_ORACLE_TIMEOUT,
PYUSD_ORACLE_ERROR,
PYUSD_USD_FEED,
PRICE_TIMEOUT,
DELAY_UNTIL_DEFAULT,
} from '../../../../test/plugins/individual-collateral/meta-morpho/constants'
import { MetaMorphoFiatCollateral } from '../../../../typechain'
import { ContractFactory, BigNumber } from 'ethers'

// Morpho Vault V2 collaterals. All are USD-pegged MetaMorpho ERC4626 vaults with
// no gates and 18-decimal shares over a 6-decimal asset (verified on-chain).
interface V2VaultDeployment {
tokenKey: keyof ITokens
feed: string
oracleTimeout: BigNumber
oracleError: BigNumber
}

const VAULTS: V2VaultDeployment[] = [
{
tokenKey: 'steakUSDCPrime',
feed: USDC_USD_FEED,
oracleTimeout: USDC_ORACLE_TIMEOUT,
oracleError: USDC_ORACLE_ERROR,
}, // eslint-disable-line prettier/prettier
{
tokenKey: 'sentoraPYUSD',
feed: PYUSD_USD_FEED,
oracleTimeout: PYUSD_ORACLE_TIMEOUT,
oracleError: PYUSD_ORACLE_ERROR,
}, // eslint-disable-line prettier/prettier
{
tokenKey: 'gauntletUSDCFrontier',
feed: USDC_USD_FEED,
oracleTimeout: USDC_ORACLE_TIMEOUT,
oracleError: USDC_ORACLE_ERROR,
}, // eslint-disable-line prettier/prettier
{
tokenKey: 'steakUSDTPrime',
feed: USDT_USD_FEED,
oracleTimeout: USDT_ORACLE_TIMEOUT,
oracleError: USDT_ORACLE_ERROR,
}, // eslint-disable-line prettier/prettier
{
tokenKey: 'galaxyUSDTQuality',
feed: USDT_USD_FEED,
oracleTimeout: USDT_ORACLE_TIMEOUT,
oracleError: USDT_ORACLE_ERROR,
}, // eslint-disable-line prettier/prettier
{
tokenKey: 'gauntletUSDCPrime',
feed: USDC_USD_FEED,
oracleTimeout: USDC_ORACLE_TIMEOUT,
oracleError: USDC_ORACLE_ERROR,
}, // eslint-disable-line prettier/prettier
{
tokenKey: 'galaxyUSDCQuality',
feed: USDC_USD_FEED,
oracleTimeout: USDC_ORACLE_TIMEOUT,
oracleError: USDC_ORACLE_ERROR,
}, // eslint-disable-line prettier/prettier
{
tokenKey: 'skyUSDTSavings',
feed: USDT_USD_FEED,
oracleTimeout: USDT_ORACLE_TIMEOUT,
oracleError: USDT_ORACLE_ERROR,
}, // eslint-disable-line prettier/prettier
]

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

console.log(`Deploying Morpho Vault V2 collaterals 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[] = []

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

for (const v of VAULTS) {
const erc20 = networkConfig[chainId].tokens[v.tokenKey]
if (!erc20) {
throw new Error(`Missing token address for ${v.tokenKey} on chain ${chainId}`)
}

const collateral = <MetaMorphoFiatCollateral>await MetaMorphoFiatCollateralFactory.connect(
deployer
).deploy(
{
priceTimeout: PRICE_TIMEOUT.toString(),
chainlinkFeed: v.feed,
oracleError: v.oracleError.toString(),
erc20: erc20,
maxTradeVolume: fp('1e6').toString(),
oracleTimeout: v.oracleTimeout.toString(),
targetName: hre.ethers.utils.formatBytes32String('USD'),
defaultThreshold: v.oracleError.add(fp('0.01')).toString(), // +1% buffer rule
delayUntilDefault: DELAY_UNTIL_DEFAULT.toString(),
},
fp('1e-4') // can have mild drawdowns
)
await collateral.deployed()

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

assetCollDeployments.collateral[v.tokenKey] = collateral.address
assetCollDeployments.erc20s[v.tokenKey] = erc20
deployedCollateral.push(collateral.address.toString())

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

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

main().catch((error) => {
console.error(error)
process.exitCode = 1
})
137 changes: 137 additions & 0 deletions scripts/verification/collateral-plugins/verify_morpho_v2_vaults.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import hre from 'hardhat'
import { getChainId } from '../../../common/blockchain-utils'
import { developmentChains, networkConfig, ITokens } from '../../../common/configuration'
import { fp } from '../../../common/numbers'
import {
getDeploymentFile,
getAssetCollDeploymentFilename,
IAssetCollDeployments,
} from '../../deployment/common'
import {
USDC_ORACLE_TIMEOUT,
USDC_ORACLE_ERROR,
USDC_USD_FEED,
USDT_ORACLE_TIMEOUT,
USDT_ORACLE_ERROR,
USDT_USD_FEED,
PYUSD_ORACLE_TIMEOUT,
PYUSD_ORACLE_ERROR,
PYUSD_USD_FEED,
PRICE_TIMEOUT,
DELAY_UNTIL_DEFAULT,
} from '../../../test/plugins/individual-collateral/meta-morpho/constants'
import { verifyContract } from '../../deployment/utils'
import { BigNumber } from 'ethers'

let deployments: IAssetCollDeployments

// Morpho Vault V2 collaterals — must match the args used in deploy_morpho_v2_vaults.ts
interface V2VaultVerification {
tokenKey: keyof ITokens
feed: string
oracleTimeout: BigNumber
oracleError: BigNumber
}

const VAULTS: V2VaultVerification[] = [
{
tokenKey: 'steakUSDCPrime',
feed: USDC_USD_FEED,
oracleTimeout: USDC_ORACLE_TIMEOUT,
oracleError: USDC_ORACLE_ERROR,
}, // eslint-disable-line prettier/prettier
{
tokenKey: 'sentoraPYUSD',
feed: PYUSD_USD_FEED,
oracleTimeout: PYUSD_ORACLE_TIMEOUT,
oracleError: PYUSD_ORACLE_ERROR,
}, // eslint-disable-line prettier/prettier
{
tokenKey: 'gauntletUSDCFrontier',
feed: USDC_USD_FEED,
oracleTimeout: USDC_ORACLE_TIMEOUT,
oracleError: USDC_ORACLE_ERROR,
}, // eslint-disable-line prettier/prettier
{
tokenKey: 'steakUSDTPrime',
feed: USDT_USD_FEED,
oracleTimeout: USDT_ORACLE_TIMEOUT,
oracleError: USDT_ORACLE_ERROR,
}, // eslint-disable-line prettier/prettier
{
tokenKey: 'galaxyUSDTQuality',
feed: USDT_USD_FEED,
oracleTimeout: USDT_ORACLE_TIMEOUT,
oracleError: USDT_ORACLE_ERROR,
}, // eslint-disable-line prettier/prettier
{
tokenKey: 'gauntletUSDCPrime',
feed: USDC_USD_FEED,
oracleTimeout: USDC_ORACLE_TIMEOUT,
oracleError: USDC_ORACLE_ERROR,
}, // eslint-disable-line prettier/prettier
{
tokenKey: 'galaxyUSDCQuality',
feed: USDC_USD_FEED,
oracleTimeout: USDC_ORACLE_TIMEOUT,
oracleError: USDC_ORACLE_ERROR,
}, // eslint-disable-line prettier/prettier
{
tokenKey: 'skyUSDTSavings',
feed: USDT_USD_FEED,
oracleTimeout: USDT_ORACLE_TIMEOUT,
oracleError: USDT_ORACLE_ERROR,
}, // eslint-disable-line prettier/prettier
]

async function main() {
// ********** Read config **********
const chainId = await getChainId(hre)
if (!networkConfig[chainId]) {
throw new Error(`Missing network configuration for ${hre.network.name}`)
}

if (developmentChains.includes(hre.network.name)) {
throw new Error(`Cannot verify contracts for development chain ${hre.network.name}`)
}

const assetCollDeploymentFilename = getAssetCollDeploymentFilename(chainId)
deployments = <IAssetCollDeployments>getDeploymentFile(assetCollDeploymentFilename)

for (const v of VAULTS) {
/******** Verify Morpho Vault V2 collateral **************************/
const collateralAddr = deployments.collateral[v.tokenKey]
if (!collateralAddr) {
throw new Error(`Missing deployed collateral for ${v.tokenKey} on chain ${chainId}`)
}
const erc20 = networkConfig[chainId].tokens[v.tokenKey]
if (!erc20) {
throw new Error(`Missing token address for ${v.tokenKey} on chain ${chainId}`)
}

await verifyContract(
chainId,
collateralAddr,
[
{
priceTimeout: PRICE_TIMEOUT.toString(),
chainlinkFeed: v.feed,
oracleError: v.oracleError.toString(),
erc20: erc20,
maxTradeVolume: fp('1e6').toString(),
oracleTimeout: v.oracleTimeout.toString(),
targetName: hre.ethers.utils.formatBytes32String('USD'),
defaultThreshold: v.oracleError.add(fp('0.01')).toString(), // +1% buffer rule
delayUntilDefault: DELAY_UNTIL_DEFAULT.toString(),
},
fp('1e-4'), // can have small drawdowns
],
'contracts/plugins/assets/meta-morpho/MetaMorphoFiatCollateral.sol:MetaMorphoFiatCollateral'
)
}
}

main().catch((error) => {
console.error(error)
process.exitCode = 1
})
1 change: 1 addition & 0 deletions scripts/verify_etherscan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ async function main() {
'collateral-plugins/verify_sfrax_eth.ts',
'collateral-plugins/verify_steakusdc.ts',
'collateral-plugins/verify_re7weth.ts',
'collateral-plugins/verify_morpho_v2_vaults.ts',
'collateral-plugins/verify_ethx.ts',
'collateral-plugins/verify_apxeth.ts',
'collateral-plugins/verify_USDe.ts',
Expand Down
Loading
Loading