Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
72 changes: 72 additions & 0 deletions script/deploy/mainnet/044_DeployUSDCMorphoMarketScript.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;

// Contracts
import {Proxy} from "contracts/Proxy.sol";
import {Mainnet} from "contracts/utils/Addresses.sol";
import {MultiAssetARM} from "contracts/MultiAssetARM.sol";
import {MorphoMarket} from "contracts/markets/MorphoMarket.sol";
import {Abstract4626MarketWrapper} from "contracts/markets/Abstract4626MarketWrapper.sol";

// Deployment
import {AbstractDeployScript} from "script/deploy/helpers/AbstractDeployScript.s.sol";

/// @title Deploy the USDC ARM Morpho market
/// @notice Deploys a Morpho Vault V2 wrapper for the USDC ARM and registers it as a supported
/// market. The mainnet 5/8 multisig owns the wrapper, while the 2/8 multisig harvests its
/// rewards. The market is not activated automatically.
/// @dev The 5/8 registration action is simulated in _fork(); on mainnet it is executed separately
/// by the multisig after the proxy and implementation have been deployed. No governance
/// proposal is required because the 5/8 multisig directly owns the USDC ARM.
///
/// Registration and activation are deliberately separate. Before activation, operators must
/// verify that the Vault V2 gates permit the wrapper and ARM, and that every cap used by the
/// liquidity adapter has enough headroom for the ARM's full one-transaction allocation. Vault
/// V2 automatically forwards deposits to its liquidity adapter, so an otherwise valid ARM
/// allocation can revert when an absolute or relative cap is reached.
///
/// Adapter safety is an ongoing operational assumption. The target currently uses a direct,
/// loss-aware MorphoMarketV1AdapterV2, but its curator can add adapters after deployment. While
/// this ARM market is active, operators must monitor adapter changes and deactivate it before
/// any MorphoVaultV1Adapter over MetaMorpho V1.1 becomes effective, because its lostAssets
/// accounting can keep bad debt in the reported Vault V2 share price.
///
/// Operators must also verify downstream liquidity. MorphoMarket preserves Vault V2's
/// maxWithdraw/maxRedeem values as redeemability signals, which can be lower than the full
/// economic position. The ARM values that position separately through convertToAssets. Vault V2
/// snapshots its exchange rate on the first accrual in each transaction, so a downstream loss
/// occurring later in the transaction is reflected starting from the next transaction.
contract $044_DeployUSDCMorphoMarketScript is AbstractDeployScript("044_DeployUSDCMorphoMarketScript") {
function _execute() internal override {
address usdcARM = resolver.resolve("USDC_ARM");

// 1. Deploy the Morpho Vault V2 market proxy.
Proxy morphoMarketProxy = new Proxy();
_recordDeployment("MORPHO_MARKET_USDC_ARM", address(morphoMarketProxy));

// 2. Deploy the implementation for the USDC ARM and Wintermute USDC Prime vault.
MorphoMarket morphoMarketImpl = new MorphoMarket(usdcARM, Mainnet.MORPHO_WINTERMUTE_USDC_PRIME_VAULT);
_recordDeployment("MORPHO_MARKET_USDC_ARM_IMPL", address(morphoMarketImpl));

// 3. Initialize the wrapper and hand ownership to the USDC ARM's 5/8 multisig.
// The deployment does not activate the market or deposit USDC into Vault V2.
bytes memory data = abi.encodeWithSelector(
Abstract4626MarketWrapper.initialize.selector, Mainnet.MULTISIG_2_OF_8, Mainnet.MERKLE_DISTRIBUTOR
);
morphoMarketProxy.initialize(address(morphoMarketImpl), Mainnet.MULTISIG_5_OF_8, data);
}

function _fork() internal override {
MultiAssetARM usdcARM = MultiAssetARM(payable(resolver.resolve("USDC_ARM")));
address morphoMarket = resolver.resolve("MORPHO_MARKET_USDC_ARM");

// Idempotent: the deployment runner can replay the pending multisig action on forks.
if (usdcARM.supportedMarkets(morphoMarket)) return;

address[] memory markets = new address[](1);
markets[0] = morphoMarket;

vm.prank(Mainnet.MULTISIG_5_OF_8);
usdcARM.addMarkets(markets);
}
}
1 change: 1 addition & 0 deletions src/contracts/utils/Addresses.sol
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ library Mainnet {

// Morpho Vaults
address public constant MORPHO_WETH_VAULT = 0x3Dfe70B05657949A5dB340754aD664810ac63b21;
address public constant MORPHO_WINTERMUTE_USDC_PRIME_VAULT = 0x5dc53a23AdC9f2Bed98de6F59F7F309a7c71FF2B;

// Origin
address public constant OETH_VAULT = 0x39254033945AA2E4809Cc2977E7087BEE48bd7Ab;
Expand Down
53 changes: 49 additions & 4 deletions test/smoke/PaxosARMSmokeTest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ pragma solidity ^0.8.23;

import {AbstractSmokeTest} from "./AbstractSmokeTest.sol";

import {IERC20} from "contracts/Interfaces.sol";
import {IERC20, IERC4626} from "contracts/Interfaces.sol";
import {MultiAssetARM} from "contracts/MultiAssetARM.sol";
import {PaxosAssetAdapter} from "contracts/adapters/PaxosAssetAdapter.sol";
import {CapManager} from "contracts/CapManager.sol";
import {Proxy} from "contracts/Proxy.sol";
import {Mainnet} from "contracts/utils/Addresses.sol";
import {MorphoMarket} from "contracts/markets/MorphoMarket.sol";

contract Fork_PaxosARM_Smoke_Test is AbstractSmokeTest {
IERC20 usdc;
Expand Down Expand Up @@ -77,9 +78,6 @@ contract Fork_PaxosARM_Smoke_Test is AbstractSmokeTest {
assertEq(capManager.arm(), address(usdcARM), "cap manager arm");
assertEq(capManager.totalAssetsCap(), 1_000_000e18, "total assets cap");
assertEq(capManager.accountCapEnabled(), true, "account cap enabled");
assertEq(
capManager.liquidityProviderCaps(Mainnet.TREASURY_LP), 1_000_000e18 - 100_000e6, "liquidity provider cap"
);
assertEq(capManager.operator(), operator, "cap manager operator");
assertEq(capManager.owner(), Mainnet.MULTISIG_2_OF_8, "cap manager owner");
}
Expand All @@ -89,6 +87,53 @@ contract Fork_PaxosARM_Smoke_Test is AbstractSmokeTest {
_assertBaseAssetConfig(Mainnet.USDG, address(usdgAdapter), "USDG");
}

function test_morphoMarketConfig() external view {
MorphoMarket morphoMarket = MorphoMarket(resolver.resolve("MORPHO_MARKET_USDC_ARM"));

assertEq(morphoMarket.arm(), address(usdcARM), "market arm");
assertEq(morphoMarket.asset(), Mainnet.USDC, "market asset");
assertEq(morphoMarket.market(), Mainnet.MORPHO_WINTERMUTE_USDC_PRIME_VAULT, "configured Morpho Vault V2");
assertEq(morphoMarket.owner(), Mainnet.MULTISIG_5_OF_8, "market owner");
assertEq(morphoMarket.harvester(), Mainnet.MULTISIG_2_OF_8, "market harvester");
assertEq(address(morphoMarket.merkleDistributor()), Mainnet.MERKLE_DISTRIBUTOR, "Merkle distributor");
assertTrue(usdcARM.supportedMarkets(address(morphoMarket)), "market supported");
assertEq(usdcARM.activeMarket(), address(0), "market not activated");
}

function test_morphoMarketDepositWithdraw() external {
MorphoMarket morphoMarket = MorphoMarket(resolver.resolve("MORPHO_MARKET_USDC_ARM"));
// This amount fits the Vault V2 cap headroom and downstream liquidity at the smoke-test fork.
// A direct withdrawal can succeed even when the max functions report less than the economic position.
uint256 depositAmount = 10_000e6;

deal(address(usdc), address(usdcARM), depositAmount);
vm.startPrank(address(usdcARM));
usdc.approve(address(morphoMarket), depositAmount);
uint256 shares = morphoMarket.deposit(depositAmount, address(usdcARM));
vm.stopPrank();

assertGt(shares, 0, "vault shares minted");
assertEq(morphoMarket.balanceOf(address(usdcARM)), shares, "wrapper share balance");

IERC4626 vault = IERC4626(morphoMarket.market());
uint256 economicAssets = morphoMarket.convertToAssets(shares);
uint256 redeemableShares = morphoMarket.maxRedeem(address(usdcARM));
uint256 redeemableAssets = morphoMarket.maxWithdraw(address(usdcARM));

assertApproxEqAbs(economicAssets, depositAmount, 1, "economic position value");
assertEq(redeemableShares, vault.maxRedeem(address(morphoMarket)), "max redeem delegated to vault");
assertEq(redeemableAssets, vault.maxWithdraw(address(morphoMarket)), "max withdraw delegated to vault");
assertLt(redeemableShares, shares, "redeemable shares below economic position");
assertLt(redeemableAssets, economicAssets, "redeemable assets below economic position");

uint256 balanceBefore = usdc.balanceOf(address(usdcARM));
vm.prank(address(usdcARM));
uint256 burnedShares = morphoMarket.withdraw(economicAssets, address(usdcARM), address(usdcARM));

assertGt(burnedShares, 0, "vault shares burned");
assertEq(usdc.balanceOf(address(usdcARM)), balanceBefore + economicAssets, "USDC returned to ARM");
}

function _assertBaseAssetConfig(address baseAsset, address expectedAdapter, string memory label) internal view {
(,,,,,, bool peggedToLiquidityAsset, uint8 baseAssetDecimals, address adapter) =
usdcARM.baseAssetConfigs(baseAsset);
Expand Down
Loading