|
| 1 | +// SPDX-License-Identifier: UNLICENSED |
| 2 | +pragma solidity ^0.8.24; |
| 3 | + |
| 4 | +import {FCMVault} from "../../src/FCMVault.sol"; |
| 5 | +import {IFCMVault} from "../../src/interfaces/IFCMVault.sol"; |
| 6 | +import {ISwapRouter02} from "../../src/interfaces/external/ISwapRouter02.sol"; |
| 7 | +import {IUniswapV3Pool} from "../../src/interfaces/external/IUniswapV3Pool.sol"; |
| 8 | +import {IMorpho, Id, Market, MarketParams, Position} from "@morpho-blue/interfaces/IMorpho.sol"; |
| 9 | +import {IOracle} from "@morpho-blue/interfaces/IOracle.sol"; |
| 10 | +import {MarketParamsLib} from "@morpho-blue/libraries/MarketParamsLib.sol"; |
| 11 | +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; |
| 12 | +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; |
| 13 | +import {Test} from "forge-std/Test.sol"; |
| 14 | + |
| 15 | +contract ForkDeployers is Test { |
| 16 | + IERC20 constant WBTC = IERC20(0x717DAE2BaF7656BE9a9B01deE31d571a9d4c9579); |
| 17 | + IERC20 constant PYUSD0 = IERC20(0x99aF3EeA856556646C98c8B9b2548Fe815240750); |
| 18 | + IERC20 constant FUSDEV = IERC20(0xd069d989e2F44B70c65347d1853C0c67e10a9F8D); |
| 19 | + address constant MARKET_ORACLE = 0x5B3e0BA14443B444D557C0C2F85592d88B88f5c8; |
| 20 | + address constant MARKET_IRM = 0xdFC4f7951EcDd2D505b6406e9c886c0dB9393546; |
| 21 | + IOracle constant YIELD_ORACLE = IOracle(0x144F613490DD55C9844Ef139CFB9B63433dD349F); |
| 22 | + address constant SWAP_FACTORY = 0xca6d7Bb03334bBf135902e1d919a5feccb461632; |
| 23 | + IMorpho constant MORPHO = IMorpho(0x9a094eA4AbE343D908E1bDE9fC478D71b41D665f); |
| 24 | + ISwapRouter02 constant SWAP_ROUTER = ISwapRouter02(0xeEDC6Ff75e1b10B903D9013c358e446a73d35341); |
| 25 | + |
| 26 | + address constant YIELD_LOAN_POOL = 0x9196e243b7562B0866309013f2F9EB63F83A690f; |
| 27 | + |
| 28 | + uint256 constant HEALTH_FACTOR_MIN = 1_228_571_428_571_428_571; |
| 29 | + uint256 constant HEALTH_FACTOR_MIN_TARGET = 1_230_329_041_487_839_771; |
| 30 | + uint256 constant HEALTH_FACTOR_MAX = 1_433_333_333_333_333_333; |
| 31 | + uint256 constant HEALTH_FACTOR_MAX_TARGET = 1_430_948_419_301_164_725; |
| 32 | + uint256 constant MARKET_LLTV = 0.86e18; |
| 33 | + uint256 constant YIELD_FACTOR_MAX = 1.01e18; |
| 34 | + uint24 constant COLLATERAL_LOAN_POOL_FEE = 3000; |
| 35 | + uint24 constant YIELD_LOAN_POOL_FEE = 100; |
| 36 | + |
| 37 | + uint256 constant ORACLE_PRICE = 100_000e36; |
| 38 | + uint256 constant YIELD_ORACLE_PRICE = 1e24; |
| 39 | + |
| 40 | + FCMVault internal vault; |
| 41 | + MarketParams internal mp; |
| 42 | + Id internal marketId; |
| 43 | + address internal collateralLoanPool; |
| 44 | + uint160 internal cleanSpot; |
| 45 | + |
| 46 | + address internal owner = address(this); |
| 47 | + address internal arb = makeAddr("arb"); |
| 48 | + address internal alice = makeAddr("alice"); |
| 49 | + |
| 50 | + function _forkSetup() internal { |
| 51 | + vm.createSelectFork("http://localhost:8545"); |
| 52 | + |
| 53 | + setCollateralPrice(ORACLE_PRICE); |
| 54 | + setYieldPrice(YIELD_ORACLE_PRICE); |
| 55 | + |
| 56 | + collateralLoanPool = _getPool(SWAP_FACTORY, address(WBTC), address(PYUSD0), COLLATERAL_LOAN_POOL_FEE); |
| 57 | + require(collateralLoanPool != address(0), "WBTC/PYUSD0 pool missing"); |
| 58 | + |
| 59 | + mp = MarketParams({ |
| 60 | + loanToken: address(PYUSD0), |
| 61 | + collateralToken: address(WBTC), |
| 62 | + oracle: MARKET_ORACLE, |
| 63 | + irm: MARKET_IRM, |
| 64 | + lltv: MARKET_LLTV |
| 65 | + }); |
| 66 | + marketId = MarketParamsLib.id(mp); |
| 67 | + |
| 68 | + _supplyMorphoLiquidity(100_000_000_000e6); |
| 69 | + |
| 70 | + (cleanSpot,,,,,,) = IUniswapV3Pool(YIELD_LOAN_POOL).slot0(); |
| 71 | + |
| 72 | + vault = new FCMVault( |
| 73 | + IFCMVault.InitParams({ |
| 74 | + collateralToken: WBTC, |
| 75 | + loanToken: PYUSD0, |
| 76 | + yieldToken: FUSDEV, |
| 77 | + healthFactorMin: HEALTH_FACTOR_MIN, |
| 78 | + healthFactorMinTarget: HEALTH_FACTOR_MIN_TARGET, |
| 79 | + healthFactorMax: HEALTH_FACTOR_MAX, |
| 80 | + healthFactorMaxTarget: HEALTH_FACTOR_MAX_TARGET, |
| 81 | + yieldFactorMax: YIELD_FACTOR_MAX, |
| 82 | + collateralLoanPool: collateralLoanPool, |
| 83 | + collateralLoanPoolFee: COLLATERAL_LOAN_POOL_FEE, |
| 84 | + yieldLoanPool: YIELD_LOAN_POOL, |
| 85 | + yieldLoanPoolFee: YIELD_LOAN_POOL_FEE, |
| 86 | + marketOracle: MARKET_ORACLE, |
| 87 | + marketIrm: MARKET_IRM, |
| 88 | + marketLltv: MARKET_LLTV, |
| 89 | + yieldOracle: YIELD_ORACLE, |
| 90 | + morpho: MORPHO, |
| 91 | + swapRouter: SWAP_ROUTER, |
| 92 | + owner: owner, |
| 93 | + name: "fcmWBTC-fork", |
| 94 | + symbol: "fcmWBTC-F" |
| 95 | + }) |
| 96 | + ); |
| 97 | + vault.setMaxTvl(type(uint256).max); |
| 98 | + vault.setMaxSlippageBps(100); |
| 99 | + } |
| 100 | + |
| 101 | + function _supplyMorphoLiquidity(uint256 amount) internal { |
| 102 | + address supplier = makeAddr("supplier"); |
| 103 | + deal(address(PYUSD0), supplier, amount); |
| 104 | + vm.startPrank(supplier); |
| 105 | + PYUSD0.approve(address(MORPHO), type(uint256).max); |
| 106 | + MORPHO.supply(mp, amount, 0, supplier, ""); |
| 107 | + vm.stopPrank(); |
| 108 | + } |
| 109 | + |
| 110 | + function _fundArb() internal { |
| 111 | + vm.prank(owner); |
| 112 | + vault.grantEarlyAccess(arb); |
| 113 | + deal(address(WBTC), arb, 100_000_000e8); |
| 114 | + deal(address(PYUSD0), arb, 100_000_000e6); |
| 115 | + deal(address(FUSDEV), arb, 100_000_000e18); |
| 116 | + vm.startPrank(arb); |
| 117 | + WBTC.approve(address(vault), type(uint256).max); |
| 118 | + PYUSD0.approve(address(SWAP_ROUTER), type(uint256).max); |
| 119 | + FUSDEV.approve(address(SWAP_ROUTER), type(uint256).max); |
| 120 | + vm.stopPrank(); |
| 121 | + } |
| 122 | + |
| 123 | + function _depositUsers(uint256 nUsers, uint256 depositAmount) internal { |
| 124 | + for (uint256 i = 0; i < nUsers; i++) { |
| 125 | + address u = makeAddr(string.concat("user", vm.toString(i))); |
| 126 | + vault.grantEarlyAccess(u); |
| 127 | + deal(address(WBTC), u, depositAmount); |
| 128 | + vm.startPrank(u); |
| 129 | + WBTC.approve(address(vault), depositAmount); |
| 130 | + vault.deposit(depositAmount, u); |
| 131 | + vm.stopPrank(); |
| 132 | + _arbPoolToSpot(); |
| 133 | + } |
| 134 | + } |
| 135 | + |
| 136 | + function setCollateralPrice(uint256 price) internal { |
| 137 | + vm.mockCall(MARKET_ORACLE, abi.encodeWithSelector(IOracle.price.selector), abi.encode(price)); |
| 138 | + } |
| 139 | + |
| 140 | + function setYieldPrice(uint256 price) internal { |
| 141 | + vm.mockCall(address(YIELD_ORACLE), abi.encodeWithSelector(IOracle.price.selector), abi.encode(price)); |
| 142 | + } |
| 143 | + |
| 144 | + function _arbPoolToSpot() internal { |
| 145 | + (uint160 currentSpot,,,,,,) = IUniswapV3Pool(YIELD_LOAN_POOL).slot0(); |
| 146 | + if (currentSpot < cleanSpot) { |
| 147 | + vm.prank(arb); |
| 148 | + ISwapRouter02(address(SWAP_ROUTER)) |
| 149 | + .exactInputSingle( |
| 150 | + ISwapRouter02.ExactInputSingleParams({ |
| 151 | + tokenIn: address(FUSDEV), |
| 152 | + tokenOut: address(PYUSD0), |
| 153 | + fee: YIELD_LOAN_POOL_FEE, |
| 154 | + recipient: arb, |
| 155 | + amountIn: 1e6, |
| 156 | + amountOutMinimum: 0, |
| 157 | + sqrtPriceLimitX96: cleanSpot |
| 158 | + }) |
| 159 | + ); |
| 160 | + } else if (currentSpot > cleanSpot) { |
| 161 | + vm.prank(arb); |
| 162 | + ISwapRouter02(address(SWAP_ROUTER)) |
| 163 | + .exactInputSingle( |
| 164 | + ISwapRouter02.ExactInputSingleParams({ |
| 165 | + tokenIn: address(PYUSD0), |
| 166 | + tokenOut: address(FUSDEV), |
| 167 | + fee: YIELD_LOAN_POOL_FEE, |
| 168 | + recipient: arb, |
| 169 | + amountIn: 1e6, |
| 170 | + amountOutMinimum: 0, |
| 171 | + sqrtPriceLimitX96: cleanSpot |
| 172 | + }) |
| 173 | + ); |
| 174 | + } |
| 175 | + } |
| 176 | + |
| 177 | + function _tvlUsd() internal view returns (uint256) { |
| 178 | + return Math.mulDiv(vault.totalAssets(), IOracle(MARKET_ORACLE).price(), 1e36); |
| 179 | + } |
| 180 | + |
| 181 | + function _hf() internal view returns (uint256) { |
| 182 | + Position memory pos = MORPHO.position(marketId, address(vault)); |
| 183 | + if (pos.borrowShares == 0) return type(uint256).max; |
| 184 | + Market memory mkt = MORPHO.market(marketId); |
| 185 | + uint256 debt = Math.mulDiv( |
| 186 | + uint256(pos.borrowShares), |
| 187 | + uint256(mkt.totalBorrowAssets) + 1, |
| 188 | + uint256(mkt.totalBorrowShares) + 1e6, |
| 189 | + Math.Rounding.Ceil |
| 190 | + ); |
| 191 | + uint256 maxBorrow = |
| 192 | + Math.mulDiv(uint256(pos.collateral), Math.mulDiv(IOracle(MARKET_ORACLE).price(), MARKET_LLTV, 1e36), 1e18); |
| 193 | + return Math.mulDiv(maxBorrow, 1e18, debt); |
| 194 | + } |
| 195 | + |
| 196 | + function _debt() internal view returns (uint256) { |
| 197 | + Position memory pos = MORPHO.position(marketId, address(vault)); |
| 198 | + if (pos.borrowShares == 0) return 0; |
| 199 | + Market memory mkt = MORPHO.market(marketId); |
| 200 | + return Math.mulDiv( |
| 201 | + uint256(pos.borrowShares), |
| 202 | + uint256(mkt.totalBorrowAssets) + 1, |
| 203 | + uint256(mkt.totalBorrowShares) + 1e6, |
| 204 | + Math.Rounding.Ceil |
| 205 | + ); |
| 206 | + } |
| 207 | + |
| 208 | + function _getPool(address factory, address tokenA, address tokenB, uint24 fee) internal view returns (address) { |
| 209 | + (bool ok, bytes memory data) = factory.staticcall(abi.encodeWithSelector(0x1698ee82, tokenA, tokenB, fee)); |
| 210 | + require(ok, "factory call failed"); |
| 211 | + return abi.decode(data, (address)); |
| 212 | + } |
| 213 | +} |
0 commit comments