diff --git a/solidity/foundry.toml b/solidity/foundry.toml index 39fe413..0bab854 100644 --- a/solidity/foundry.toml +++ b/solidity/foundry.toml @@ -17,7 +17,7 @@ fs_permissions = [{ access = "read", path = "./deployments" }] flow_mainnet = "https://mainnet.evm.nodes.onflow.org" [fmt] -line_length = 100 +line_length = 120 tab_width = 4 # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/solidity/script/ConfiguredScript.s.sol b/solidity/script/ConfiguredScript.s.sol index 7add117..35d3bd9 100644 --- a/solidity/script/ConfiguredScript.s.sol +++ b/solidity/script/ConfiguredScript.s.sol @@ -51,8 +51,7 @@ abstract contract ConfiguredScript is Script { c.chainId = vm.parseTomlUint(toml, ".chainId"); require( - c.chainId == block.chainid, - string.concat("config chainId does not match RPC chain (network=", network, ")") + c.chainId == block.chainid, string.concat("config chainId does not match RPC chain (network=", network, ")") ); c.collateral = vm.parseTomlAddress(toml, ".collateral"); @@ -94,20 +93,17 @@ abstract contract ConfiguredScript is Script { function _requireMarketExists(Config memory c) internal view returns (Market memory m) { m = MORPHO.market(_marketId(c)); require( - m.lastUpdate != 0, - "Morpho market for config params does not exist; check marketOracle/marketIrm/marketLltv" + m.lastUpdate != 0, "Morpho market for config params does not exist; check marketOracle/marketIrm/marketLltv" ); } /// @dev Both FlowSwap pools the vault trades on must exist. function _requirePoolsExist(Config memory c) internal view { - address yieldPool = - IUniswapV3Factory(c.swapFactory).getPool(c.yieldToken, c.loanToken, c.feeYieldDebt); + address yieldPool = IUniswapV3Factory(c.swapFactory).getPool(c.yieldToken, c.loanToken, c.feeYieldDebt); require(yieldPool != address(0), "yield/debt pool missing"); require(yieldPool == c.yieldDebtPool, "config yieldDebtPool does not match factory"); require( - IUniswapV3Factory(c.swapFactory).getPool(c.collateral, c.loanToken, c.feeAssetDebt) - != address(0), + IUniswapV3Factory(c.swapFactory).getPool(c.collateral, c.loanToken, c.feeAssetDebt) != address(0), "asset/debt pool missing" ); } diff --git a/solidity/script/LiveCheck.s.sol b/solidity/script/LiveCheck.s.sol index c879418..790783e 100644 --- a/solidity/script/LiveCheck.s.sol +++ b/solidity/script/LiveCheck.s.sol @@ -50,17 +50,9 @@ contract LiveCheck is Script { (, address caller,) = vm.readCallers(); // Pre-flight: fail with a clear reason before any value moves. - require( - vault.hasRole(vault.EARLY_ACCESS_ROLE(), caller), - "caller lacks EARLY_ACCESS_ROLE on the vault" - ); - require( - assetToken.balanceOf(caller) >= amount, - "caller does not hold CHECK_AMOUNT of the vault asset" - ); - require( - vault.maxDeposit(caller) >= amount, "TVL headroom below CHECK_AMOUNT (is maxTvl set?)" - ); + require(vault.hasRole(vault.EARLY_ACCESS_ROLE(), caller), "caller lacks EARLY_ACCESS_ROLE on the vault"); + require(assetToken.balanceOf(caller) >= amount, "caller does not hold CHECK_AMOUNT of the vault asset"); + require(vault.maxDeposit(caller) >= amount, "TVL headroom below CHECK_AMOUNT (is maxTvl set?)"); uint256 assetBefore = assetToken.balanceOf(caller); uint256 sharesBefore = vault.balanceOf(caller); @@ -70,10 +62,7 @@ contract LiveCheck is Script { assetToken.approve(address(vault), amount); uint256 shares = vault.deposit(amount, caller); require(shares > 0, "deposit minted zero shares"); - require( - vault.balanceOf(caller) == sharesBefore + shares, - "share balance does not reflect minted shares" - ); + require(vault.balanceOf(caller) == sharesBefore + shares, "share balance does not reflect minted shares"); require(vault.totalAssets() > navBefore, "vault NAV did not grow on deposit"); // Rebalance the position. Acts only when HF is outside the [min, max] @@ -93,9 +82,7 @@ contract LiveCheck is Script { "asset balance delta does not match redeem output" ); uint256 roundtripBps = redeemed * 10_000 / amount; - require( - roundtripBps >= minRoundtripBps, "round-trip value below MIN_ROUNDTRIP_BPS tolerance" - ); + require(roundtripBps >= minRoundtripBps, "round-trip value below MIN_ROUNDTRIP_BPS tolerance"); console.log("=== live check passed ==="); console.log("deposited: %s", amount); @@ -122,7 +109,6 @@ contract LiveCheck is Script { Position memory pos = MORPHO.position(mp.id(), address(vault)); if (pos.borrowShares == 0) return 0; Market memory mkt = MORPHO.market(mp.id()); - return uint256(pos.borrowShares) - .toAssetsUp(uint256(mkt.totalBorrowAssets), uint256(mkt.totalBorrowShares)); + return uint256(pos.borrowShares).toAssetsUp(uint256(mkt.totalBorrowAssets), uint256(mkt.totalBorrowShares)); } } diff --git a/solidity/script/Rebalance.s.sol b/solidity/script/Rebalance.s.sol index 4507bec..0123db0 100644 --- a/solidity/script/Rebalance.s.sol +++ b/solidity/script/Rebalance.s.sol @@ -70,8 +70,7 @@ contract Rebalance is Script { Position memory pos = MORPHO.position(mp.id(), address(vault)); if (pos.borrowShares == 0) return type(uint256).max; uint256 debt = _debtFromPosition(mp, pos); - uint256 maxBorrow = - (uint256(pos.collateral) * ((IOracle(mp.oracle).price() * mp.lltv) / 1e36)) / 1e18; + uint256 maxBorrow = (uint256(pos.collateral) * ((IOracle(mp.oracle).price() * mp.lltv) / 1e36)) / 1e18; return (maxBorrow * 1e18) / debt; } @@ -86,14 +85,9 @@ contract Rebalance is Script { /// @dev Converts borrow shares to loan-token debt with Morpho's own /// `SharesMathLib.toAssetsUp`, matching how Morpho charges debt (and /// the contract's `MarketLib.debt`). - function _debtFromPosition(MarketParams memory mp, Position memory pos) - internal - view - returns (uint256) - { + function _debtFromPosition(MarketParams memory mp, Position memory pos) internal view returns (uint256) { Market memory mkt = MORPHO.market(mp.id()); - return uint256(pos.borrowShares) - .toAssetsUp(uint256(mkt.totalBorrowAssets), uint256(mkt.totalBorrowShares)); + return uint256(pos.borrowShares).toAssetsUp(uint256(mkt.totalBorrowAssets), uint256(mkt.totalBorrowShares)); } function _market(FCMVault vault) internal view returns (MarketParams memory) { diff --git a/solidity/script/SeedMarket.s.sol b/solidity/script/SeedMarket.s.sol index 11a588c..7d8844a 100644 --- a/solidity/script/SeedMarket.s.sol +++ b/solidity/script/SeedMarket.s.sol @@ -31,10 +31,7 @@ contract SeedMarket is ConfiguredScript { vm.startBroadcast(); address supplier = _broadcaster(); - require( - IERC20(c.loanToken).balanceOf(supplier) >= amount, - "broadcaster holds less loan token than SEED_AMOUNT" - ); + require(IERC20(c.loanToken).balanceOf(supplier) >= amount, "broadcaster holds less loan token than SEED_AMOUNT"); IERC20(c.loanToken).approve(address(MORPHO), amount); (uint256 supplied,) = MORPHO.supply(_marketParams(c), amount, 0, supplier, ""); diff --git a/solidity/script/Status.s.sol b/solidity/script/Status.s.sol index 3d87519..03566a2 100644 --- a/solidity/script/Status.s.sol +++ b/solidity/script/Status.s.sol @@ -50,10 +50,8 @@ contract Status is ConfiguredScript { function _reportPools(Config memory c) internal view { console.log("--- FlowSwap pools"); - address yieldPool = - IUniswapV3Factory(c.swapFactory).getPool(c.yieldToken, c.loanToken, c.feeYieldDebt); - address assetPool = - IUniswapV3Factory(c.swapFactory).getPool(c.collateral, c.loanToken, c.feeAssetDebt); + address yieldPool = IUniswapV3Factory(c.swapFactory).getPool(c.yieldToken, c.loanToken, c.feeYieldDebt); + address assetPool = IUniswapV3Factory(c.swapFactory).getPool(c.collateral, c.loanToken, c.feeAssetDebt); console.log("yield/debt pool (fee %s): %s", c.feeYieldDebt, yieldPool); console.log("asset/debt pool (fee %s): %s", c.feeAssetDebt, assetPool); if (yieldPool != c.yieldDebtPool) { @@ -71,9 +69,7 @@ contract Status is ConfiguredScript { try IOracle(c.marketOracle).price() returns (uint256 marketPrice) { console.log("market oracle price (1e36-scaled): %s", marketPrice); } catch { - console.log( - "MARKET ORACLE REVERTING (stale Pyth feed?) -- vault operations will fail until it updates" - ); + console.log("MARKET ORACLE REVERTING (stale Pyth feed?) -- vault operations will fail until it updates"); } if (c.yieldOracle != address(0)) { console.log("yield oracle (configured): %s", c.yieldOracle); diff --git a/solidity/script/SwapForWeth.s.sol b/solidity/script/SwapForWeth.s.sol index acbd0f1..2e96808 100644 --- a/solidity/script/SwapForWeth.s.sol +++ b/solidity/script/SwapForWeth.s.sol @@ -52,8 +52,7 @@ contract SwapForWeth is ConfiguredScript { uint256 slippageBps = vm.envOr("SLIPPAGE_BPS", uint256(300)); require(slippageBps < BPS, "SLIPPAGE_BPS must be < 10000"); - address pool = - IUniswapV3Factory(c.swapFactory).getPool(address(WFLOW), weth, WFLOW_WETH_FEE); + address pool = IUniswapV3Factory(c.swapFactory).getPool(address(WFLOW), weth, WFLOW_WETH_FEE); require(pool != address(0), "no WFLOW/WETH pool at fee tier"); vm.startBroadcast(); @@ -86,11 +85,7 @@ contract SwapForWeth is ConfiguredScript { vm.stopBroadcast(); console.log("swapped %s WFLOW (wei) for %s WETH (wei)", amountIn, received); - console.log( - "min acceptable was %s; account now holds %s WETH (wei)", - minOut, - IERC20(weth).balanceOf(account) - ); + console.log("min acceptable was %s; account now holds %s WETH (wei)", minOut, IERC20(weth).balanceOf(account)); } /// @dev Spot-price-derived minimum WETH out for `amountIn` WFLOW, less diff --git a/solidity/script/UpdatePythPrices.s.sol b/solidity/script/UpdatePythPrices.s.sol index 1e71e97..84d3258 100644 --- a/solidity/script/UpdatePythPrices.s.sol +++ b/solidity/script/UpdatePythPrices.s.sol @@ -20,14 +20,10 @@ contract UpdatePythPrices is Script { IPyth internal constant PYTH = IPyth(0x2880aB155794e7179c9eE2e38200202908C17B43); - string internal constant HERMES_URL = - "https://hermes.pyth.network/v2/updates/price/latest?encoding=hex"; - string internal constant FLOW_ID = - "2fb245b9a84554a0f15aa123cbb5f64cd263b59e9a87d80148cbffab50c69f30"; - string internal constant ETH_ID = - "ff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace"; - string internal constant BTC_ID = - "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43"; + string internal constant HERMES_URL = "https://hermes.pyth.network/v2/updates/price/latest?encoding=hex"; + string internal constant FLOW_ID = "2fb245b9a84554a0f15aa123cbb5f64cd263b59e9a87d80148cbffab50c69f30"; + string internal constant ETH_ID = "ff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace"; + string internal constant BTC_ID = "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43"; address internal constant WFLOW_PYUSD_ORACLE = 0xd8848Ccc8beA82046Da0B144844118db17086af4; address internal constant WETH_PYUSD_ORACLE = 0xD744044044C0Dd0c73BeA440747115674Ebae030; @@ -61,8 +57,7 @@ contract UpdatePythPrices is Script { command[1] = "-fsSL"; command[2] = "--max-time"; command[3] = "30"; - command[4] = - string.concat(HERMES_URL, "&ids[]=", FLOW_ID, "&ids[]=", ETH_ID, "&ids[]=", BTC_ID); + command[4] = string.concat(HERMES_URL, "&ids[]=", FLOW_ID, "&ids[]=", ETH_ID, "&ids[]=", BTC_ID); string memory response = string(vm.ffi(command)); string memory updateDataHex = vm.parseJsonString(response, ".binary.data[0]"); diff --git a/solidity/src/FCMVault.sol b/solidity/src/FCMVault.sol index 76c5bd8..2d99416 100644 --- a/solidity/src/FCMVault.sol +++ b/solidity/src/FCMVault.sol @@ -167,9 +167,7 @@ contract FCMVault is ERC4626, AccessControl, Ownable2Step, IMorphoFlashLoanCallb /// @param collateralOut Collateral swept to the owner. /// @param yieldOut Yield token swept to the owner. /// @param loanOut Over-funded loan token remainder swept back to the owner. - event EmergencyRecoveryExecuted( - uint256 debtRepaid, uint256 collateralOut, uint256 yieldOut, uint256 loanOut - ); + event EmergencyRecoveryExecuted(uint256 debtRepaid, uint256 collateralOut, uint256 yieldOut, uint256 loanOut); /// @dev Deposits are frozen while a recovery is pending or after it executes. error EmergencyRecoveryActive(); @@ -208,9 +206,7 @@ contract FCMVault is ERC4626, AccessControl, Ownable2Step, IMorphoFlashLoanCallb /// @param managementFee Management fee accrued this call, in asset terms. /// @param performanceFee Performance fee accrued this call, in asset terms. /// @param feeShares Shares minted to `recipient` (dilution). - event FeesAccrued( - address indexed recipient, uint256 managementFee, uint256 performanceFee, uint256 feeShares - ); + event FeesAccrued(address indexed recipient, uint256 managementFee, uint256 performanceFee, uint256 feeShares); /// @dev Thrown when a fee rate above its hard cap is set. error InvalidFee(); @@ -275,12 +271,7 @@ contract FCMVault is ERC4626, AccessControl, Ownable2Step, IMorphoFlashLoanCallb /// @param debtPrice Loan-token price in loan token (the 1e36 scale). /// @param yieldPrice Yield-token price in loan token, 1e36-scaled. event VaultState( - uint256 collateral, - uint256 debt, - uint256 yield, - uint256 collateralPrice, - uint256 debtPrice, - uint256 yieldPrice + uint256 collateral, uint256 debt, uint256 yield, uint256 collateralPrice, uint256 debtPrice, uint256 yieldPrice ); /// @dev Emits a `VaultState` snapshot after the wrapped function body runs. @@ -299,11 +290,7 @@ contract FCMVault is ERC4626, AccessControl, Ownable2Step, IMorphoFlashLoanCallb ); } - constructor(InitParams memory p) - ERC20(p.name, p.symbol) - ERC4626(p.collateral) - Ownable(p.admin) - { + constructor(InitParams memory p) ERC20(p.name, p.symbol) ERC4626(p.collateral) Ownable(p.admin) { require(p.healthFactorMin >= MarketLib.WAD, "HF min < WAD"); require(p.healthFactorMin <= p.healthFactorMinTarget, "HF min > minTarget"); require(p.healthFactorMinTarget <= p.healthFactorMaxTarget, "HF minTarget > maxTarget"); @@ -392,9 +379,8 @@ contract FCMVault is ERC4626, AccessControl, Ownable2Step, IMorphoFlashLoanCallb // is loan-per-yield scaled by 1e36. yield=token0 -> P = loan/yield = // price/1e36; loan=token0 -> P = yield/loan = 1e36/price. uint256 yieldPrice = IOracle(yieldOracle).price(); - (uint256 numerator, uint256 denominator) = yieldIsToken0 - ? (yieldPrice, MarketLib.ORACLE_PRICE_SCALE) - : (MarketLib.ORACLE_PRICE_SCALE, yieldPrice); + (uint256 numerator, uint256 denominator) = + yieldIsToken0 ? (yieldPrice, MarketLib.ORACLE_PRICE_SCALE) : (MarketLib.ORACLE_PRICE_SCALE, yieldPrice); // Discount the price toward the side the swap moves it: a // price-decreasing swap allows down to price*(1-slip); a price-increasing @@ -578,12 +564,7 @@ contract FCMVault is ERC4626, AccessControl, Ownable2Step, IMorphoFlashLoanCallb /// @param assets Amount of underlying asset to deposit. /// @param receiver Account to credit with newly minted shares. /// @return shares Vault shares minted to `receiver`. - function deposit(uint256 assets, address receiver) - public - override - logsVaultState - returns (uint256 shares) - { + function deposit(uint256 assets, address receiver) public override logsVaultState returns (uint256 shares) { // Freeze deposits while a recovery is pending (recoveryValidAt != 0) or done // (recovered) — don't let new funds in ahead of a sweep. Redeems stay open. if (recoveryValidAt != 0 || recovered) revert EmergencyRecoveryActive(); @@ -740,9 +721,7 @@ contract FCMVault is ERC4626, AccessControl, Ownable2Step, IMorphoFlashLoanCallb // making the withdrawal hf-neutral and permitted at any health factor. // The unsold collateral is delivered to the redeemer as the asset // balance delta (see `onMorphoFlashLoan`). - MORPHO.flashLoan( - address(loanToken), debtSlice - loanGot, abi.encode(debtSlice, collSlice) - ); + MORPHO.flashLoan(address(loanToken), debtSlice - loanGot, abi.encode(debtSlice, collSlice)); } } @@ -873,8 +852,7 @@ contract FCMVault is ERC4626, AccessControl, Ownable2Step, IMorphoFlashLoanCallb uint256 currentDebt = market.debt(); uint256 maxBorrow = market.maxBorrow(); // independent of current debt balance // we compute inline here rather than use MarketLib.healthFactor to save a SLOAD - uint256 hfBefore = - currentDebt == 0 ? type(uint256).max : maxBorrow.mulDiv(MarketLib.WAD, currentDebt); + uint256 hfBefore = currentDebt == 0 ? type(uint256).max : maxBorrow.mulDiv(MarketLib.WAD, currentDebt); if (hfBefore > healthFactorMax) { _rebalanceLever(maxBorrow, currentDebt); @@ -914,10 +892,7 @@ contract FCMVault is ERC4626, AccessControl, Ownable2Step, IMorphoFlashLoanCallb /// second `MORPHO.position` SLOAD). /// @return additionalDebt Net new debt taken on in this call (the loan token /// actually swapped into yield; 0 if nothing filled). - function _rebalanceLever(uint256 maxBorrow, uint256 currentDebt) - internal - returns (uint256 additionalDebt) - { + function _rebalanceLever(uint256 maxBorrow, uint256 currentDebt) internal returns (uint256 additionalDebt) { uint256 targetDebt = maxBorrow.mulDiv(MarketLib.WAD, healthFactorMaxTarget); if (targetDebt <= currentDebt) return 0; uint256 borrowAmount = targetDebt - currentDebt; @@ -932,9 +907,7 @@ contract FCMVault is ERC4626, AccessControl, Ownable2Step, IMorphoFlashLoanCallb uint256 loanBefore = loanToken.balanceOf(address(this)); market.borrow(borrowAmount); // slither-disable-next-line unused-return -> levered amount is measured via the loanToken balance delta below, not this return - SwapLib.swapExactInToLimit( - address(loanToken), address(yieldToken), feeYieldDebt, borrowAmount, limit - ); + SwapLib.swapExactInToLimit(address(loanToken), address(yieldToken), feeYieldDebt, borrowAmount, limit); // Repay the loan token the swap left behind, so no idle loan lingers. uint256 leftover = loanToken.balanceOf(address(this)) - loanBefore; @@ -969,10 +942,7 @@ contract FCMVault is ERC4626, AccessControl, Ownable2Step, IMorphoFlashLoanCallb /// after a liquidation that wiped collateral). /// @param currentDebt Current outstanding debt. /// @return repaid Amount of loan token repaid to Morpho in this call. - function _rebalanceDelever(uint256 maxBorrow, uint256 currentDebt) - internal - returns (uint256 repaid) - { + function _rebalanceDelever(uint256 maxBorrow, uint256 currentDebt) internal returns (uint256 repaid) { // conceptually, target debt is maxBorrow / healthFactorMinTarget uint256 targetDebt = maxBorrow.mulDiv(MarketLib.WAD, healthFactorMinTarget); if (targetDebt >= currentDebt) return 0; @@ -994,9 +964,8 @@ contract FCMVault is ERC4626, AccessControl, Ownable2Step, IMorphoFlashLoanCallb // Sell yield->loan bounded by the price limit. The pool partial-fills // up to it, so a too-large delever still repays as much as the bound // allows. - uint256 loanGot = SwapLib.swapExactInToLimit( - address(yieldToken), address(loanToken), feeYieldDebt, yieldToSell, limit - ); + uint256 loanGot = + SwapLib.swapExactInToLimit(address(yieldToken), address(loanToken), feeYieldDebt, yieldToSell, limit); // Cap repayment at outstanding debt repaid = loanGot > currentDebt ? currentDebt : loanGot; @@ -1043,9 +1012,8 @@ contract FCMVault is ERC4626, AccessControl, Ownable2Step, IMorphoFlashLoanCallb // or blocks the delever leg. Same mechanism as `_rebalanceDelever`. (uint160 limit, bool ok) = _yieldDebtSwapLimit(address(yieldToken)); if (!ok) return; - uint256 loanGot = SwapLib.swapExactInToLimit( - address(yieldToken), address(loanToken), feeYieldDebt, yieldToHarvest, limit - ); + uint256 loanGot = + SwapLib.swapExactInToLimit(address(yieldToken), address(loanToken), feeYieldDebt, yieldToHarvest, limit); // A dust surplus (or a pool already at the bound) rounds the swap output to // zero; no-op rather than pass a zero amount to the next leg, which the router // and Morpho reject. @@ -1054,8 +1022,7 @@ contract FCMVault is ERC4626, AccessControl, Ownable2Step, IMorphoFlashLoanCallb // Leg 2: loan -> collateral on the asset/debt pool -- the same unbounded swap // the redeem surplus reconciliation uses. - uint256 collateralAdded = - SwapLib.swapExactIn(address(loanToken), asset(), feeAssetDebt, loanGot); + uint256 collateralAdded = SwapLib.swapExactIn(address(loanToken), asset(), feeAssetDebt, loanGot); // slither-disable-next-line incorrect-equality -> exact-zero guard: nothing realized, skip if (collateralAdded == 0) return; diff --git a/solidity/src/interfaces/ISwapRouter.sol b/solidity/src/interfaces/ISwapRouter.sol index 481fead..72fb6d8 100644 --- a/solidity/src/interfaces/ISwapRouter.sol +++ b/solidity/src/interfaces/ISwapRouter.sol @@ -27,13 +27,7 @@ interface ISwapRouter { uint160 sqrtPriceLimitX96; } - function exactInputSingle(ExactInputSingleParams calldata params) - external - payable - returns (uint256 amountOut); + function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut); - function exactOutputSingle(ExactOutputSingleParams calldata params) - external - payable - returns (uint256 amountIn); + function exactOutputSingle(ExactOutputSingleParams calldata params) external payable returns (uint256 amountIn); } diff --git a/solidity/src/libraries/MarketLib.sol b/solidity/src/libraries/MarketLib.sol index 6c65811..40961bb 100644 --- a/solidity/src/libraries/MarketLib.sol +++ b/solidity/src/libraries/MarketLib.sol @@ -151,22 +151,14 @@ library MarketLib { /// @notice Converts a collateral amount to its value in loan-token units at the current oracle price. /// @dev Does not apply LLTV; this is a raw value conversion. Use `maxBorrowFor` for the /// LLTV-discounted borrowable amount. - function collateralToDebt(MarketParams memory market, uint256 collateralAmount) - internal - view - returns (uint256) - { + function collateralToDebt(MarketParams memory market, uint256 collateralAmount) internal view returns (uint256) { if (collateralAmount == 0) return 0; return collateralAmount.mulDiv(oraclePrice(market), ORACLE_PRICE_SCALE); } /// @notice Converts a loan-token amount to its equivalent collateral-token amount at the current oracle price. /// @dev Inverse of `collateralToDebt`. Does not apply LLTV. - function debtToCollateral(MarketParams memory market, uint256 debtAmount) - internal - view - returns (uint256) - { + function debtToCollateral(MarketParams memory market, uint256 debtAmount) internal view returns (uint256) { if (debtAmount == 0) return 0; return debtAmount.mulDiv(ORACLE_PRICE_SCALE, oraclePrice(market)); } @@ -174,11 +166,7 @@ library MarketLib { /// @notice Returns the maximum loan-token amount borrowable against `collateralAmount` at the market's LLTV. /// @dev Equal to `collateralToDebt(collateralAmount) * lltv / WAD`. A position at exactly /// this debt level has a health factor of WAD (the liquidation threshold). - function maxBorrowFor(MarketParams memory market, uint256 collateralAmount) - internal - view - returns (uint256) - { + function maxBorrowFor(MarketParams memory market, uint256 collateralAmount) internal view returns (uint256) { return collateralToDebt(market, collateralAmount).mulDiv(market.lltv, WAD); } diff --git a/solidity/src/libraries/SwapLib.sol b/solidity/src/libraries/SwapLib.sol index b03f4ca..9fb53d9 100644 --- a/solidity/src/libraries/SwapLib.sol +++ b/solidity/src/libraries/SwapLib.sol @@ -8,8 +8,7 @@ import {ISwapRouter} from "../interfaces/ISwapRouter.sol"; /// Internal helpers — inlined into the caller, recipient is always /// `address(this)`. library SwapLib { - ISwapRouter internal constant SWAP_ROUTER = - ISwapRouter(0xeEDC6Ff75e1b10B903D9013c358e446a73d35341); + ISwapRouter internal constant SWAP_ROUTER = ISwapRouter(0xeEDC6Ff75e1b10B903D9013c358e446a73d35341); /// @notice Swap `amountIn` of `tokenIn` for `tokenOut` with no slippage /// bound. The router decides the realized price. Recipient is @@ -101,13 +100,10 @@ library SwapLib { /// @param amountOut Exact amount of `tokenOut` to receive. /// @param amountInMaximum Max `tokenIn` to spend; router reverts above it. /// @return amountIn Realized `tokenIn` spent. - function swapExactOut( - address tokenIn, - address tokenOut, - uint24 fee, - uint256 amountOut, - uint256 amountInMaximum - ) internal returns (uint256 amountIn) { + function swapExactOut(address tokenIn, address tokenOut, uint24 fee, uint256 amountOut, uint256 amountInMaximum) + internal + returns (uint256 amountIn) + { return SWAP_ROUTER.exactOutputSingle( ISwapRouter.ExactOutputSingleParams({ tokenIn: tokenIn, diff --git a/solidity/test/FCMVault.t.sol b/solidity/test/FCMVault.t.sol index 7b3e496..e15bbb3 100644 --- a/solidity/test/FCMVault.t.sol +++ b/solidity/test/FCMVault.t.sol @@ -276,9 +276,7 @@ contract FCMVaultTest is Test { uint256 assetsOut = vault.redeem(shares / 2, user, user); // ~half of each leg consumed (within 0.1% — virtual-share offset). - assertApproxEqRel( - WETH.balanceOf(address(MORPHO)), collateralBefore / 2, 1e15, "collateral halved" - ); + assertApproxEqRel(WETH.balanceOf(address(MORPHO)), collateralBefore / 2, 1e15, "collateral halved"); assertApproxEqRel(FUSDEV.balanceOf(address(vault)), fusdevBefore / 2, 1e15, "fusdev halved"); assertApproxEqRel(assetsOut, amount / 2, 1e15, "assetsOut approx half"); @@ -370,9 +368,7 @@ contract FCMVaultTest is Test { uint256 assetsOut = vault.redeem(shares, user, user); assertApproxEqRel(assetsOut, fairValue, 0.02e18, "full pro-rata value, not a haircut"); - assertLt( - WETH.balanceOf(address(MORPHO)), mkCollBefore, "collateral sold to cover shortfall" - ); + assertLt(WETH.balanceOf(address(MORPHO)), mkCollBefore, "collateral sold to cover shortfall"); assertEq(PYUSD0.balanceOf(address(vault)), 0, "no loan-token dust"); assertEq(vault.balanceOf(user), 0, "shares burned"); } @@ -567,9 +563,7 @@ contract FCMVaultTest is Test { uint256 healthAfter = _healthFactor(); assertGt(healthAfter, 5e18, "health factor still well above target after bob"); - assertApproxEqRel( - healthAfter, healthBefore, 1e15, "bob did not materially change health factor" - ); + assertApproxEqRel(healthAfter, healthBefore, 1e15, "bob did not materially change health factor"); } /// @dev When the position health factor is below the target (price has dropped, @@ -637,9 +631,7 @@ contract FCMVaultTest is Test { bytes32 role = vault.EARLY_ACCESS_ROLE(); vm.expectRevert( abi.encodeWithSelector( - IAccessControl.AccessControlUnauthorizedAccount.selector, - stranger, - vault.DEFAULT_ADMIN_ROLE() + IAccessControl.AccessControlUnauthorizedAccount.selector, stranger, vault.DEFAULT_ADMIN_ROLE() ) ); vm.prank(stranger); @@ -676,9 +668,7 @@ contract FCMVaultTest is Test { WETH.approve(address(vault), amount); vm.expectRevert( abi.encodeWithSelector( - IAccessControl.AccessControlUnauthorizedAccount.selector, - bob, - vault.EARLY_ACCESS_ROLE() + IAccessControl.AccessControlUnauthorizedAccount.selector, bob, vault.EARLY_ACCESS_ROLE() ) ); vault.deposit(amount, bob); @@ -716,9 +706,7 @@ contract FCMVaultTest is Test { vm.expectRevert( abi.encodeWithSelector( - IAccessControl.AccessControlUnauthorizedAccount.selector, - bob, - vault.EARLY_ACCESS_ROLE() + IAccessControl.AccessControlUnauthorizedAccount.selector, bob, vault.EARLY_ACCESS_ROLE() ) ); vault.transfer(bob, shares); @@ -741,9 +729,7 @@ contract FCMVaultTest is Test { vm.expectRevert( abi.encodeWithSelector( - IAccessControl.AccessControlUnauthorizedAccount.selector, - user, - vault.EARLY_ACCESS_ROLE() + IAccessControl.AccessControlUnauthorizedAccount.selector, user, vault.EARLY_ACCESS_ROLE() ) ); vm.prank(user); @@ -1006,9 +992,7 @@ contract FCMVaultTest is Test { (uint160 loStart,) = h.exposed_yieldDebtSwapLimit(address(PYUSD0)); // zeroForOne (uint160 hiStart,) = h.exposed_yieldDebtSwapLimit(address(FUSDEV)); // oneForZero assertEq(uint256(loStart), sqrtFloor, "zeroForOne = Q96*sqrt(1-slip)"); - assertEq( - uint256(hiStart), Math.mulDiv(q96, q96, sqrtFloor), "oneForZero = Q96/sqrt(1-slip)" - ); + assertEq(uint256(hiStart), Math.mulDiv(q96, q96, sqrtFloor), "oneForZero = Q96/sqrt(1-slip)"); // The two directions differ by exactly the slippage factor 1/(1-slip). assertApproxEqRel( @@ -1025,9 +1009,7 @@ contract FCMVaultTest is Test { _setPoolPrice(1, 4); (uint160 lo,) = h.exposed_yieldDebtSwapLimit(address(PYUSD0)); (uint160 hi,) = h.exposed_yieldDebtSwapLimit(address(FUSDEV)); - assertApproxEqRel( - uint256(lo) * uint256(hi), _priceX192(1, 4), 1e12, "product ~= P_oracle*2**192" - ); + assertApproxEqRel(uint256(lo) * uint256(hi), _priceX192(1, 4), 1e12, "product ~= P_oracle*2**192"); } /// @notice The swap-limit feasibility flag flips with the pool's live spot: @@ -1206,10 +1188,7 @@ contract FCMVaultTest is Test { assertGt(FUSDEV.balanceOf(address(vault)), yieldBefore, "rebalance bought more yield"); assertEq(PYUSD0.balanceOf(address(vault)), 0, "no idle loan token after rebalance"); assertApproxEqRel( - _healthFactor(), - HEALTH_FACTOR_MAX_TARGET, - 1e15, - "rebalance restored HF to upper re-entry target" + _healthFactor(), HEALTH_FACTOR_MAX_TARGET, 1e15, "rebalance restored HF to upper re-entry target" ); // Redeem everything. @@ -1251,8 +1230,7 @@ contract FCMVaultTest is Test { vm.prank(user); vault.redeem(shares / 2, user, user); - (uint256 collateral, uint256 debt, uint256 yield,,,) = - _assertVaultStateMatchesCurrentState(); + (uint256 collateral, uint256 debt, uint256 yield,,,) = _assertVaultStateMatchesCurrentState(); // A partial redeem leaves a live position behind. assertGt(collateral, 0, "collateral remains"); assertGt(debt, 0, "debt remains"); @@ -1295,8 +1273,7 @@ contract FCMVaultTest is Test { vm.recordLogs(); vault.rebalance(); - (,, uint256 yield, uint256 collPrice,, uint256 yieldPrice) = - _assertVaultStateMatchesCurrentState(); + (,, uint256 yield, uint256 collPrice,, uint256 yieldPrice) = _assertVaultStateMatchesCurrentState(); assertEq(collPrice, 2300e36, "snapshot collateral price is the new oracle price"); assertEq(yieldPrice, YIELD_PRICE, "snapshot yield price"); assertGt(yield, 0, "yield leg present"); @@ -1402,8 +1379,8 @@ contract FCMVaultTest is Test { Position memory pos = MORPHO.position(marketId, address(vault)); if (pos.borrowShares == 0) return 0; Market memory mkt = MORPHO.market(marketId); - return (uint256(pos.borrowShares) * (uint256(mkt.totalBorrowAssets) + 1)) - / (uint256(mkt.totalBorrowShares) + 1e6); + return + (uint256(pos.borrowShares) * (uint256(mkt.totalBorrowAssets) + 1)) / (uint256(mkt.totalBorrowShares) + 1e6); } /// @dev Simulate a liquidation of the vault's position via the MockMorpho @@ -1412,12 +1389,7 @@ contract FCMVaultTest is Test { function _liquidate(uint256 seizedCollateral, uint256 repaidAssets) internal { (address lt, address ct, address oracle, address irm, uint256 lltv_) = vault.market(); MockMorpho(address(MORPHO)) - .liquidate( - MarketParams(lt, ct, oracle, irm, lltv_), - address(vault), - seizedCollateral, - repaidAssets - ); + .liquidate(MarketParams(lt, ct, oracle, irm, lltv_), address(vault), seizedCollateral, repaidAssets); } function _baseParams() internal view returns (FCMVault.InitParams memory) { @@ -1450,11 +1422,10 @@ contract FCMVaultTest is Test { Position memory pos = MORPHO.position(marketId, address(vault)); Market memory mkt = MORPHO.market(marketId); if (pos.borrowShares == 0) return type(uint256).max; - uint256 debt = (uint256(pos.borrowShares) * (uint256(mkt.totalBorrowAssets) + 1)) - / (uint256(mkt.totalBorrowShares) + 1e6); - uint256 maxBorrow = Math.mulDiv( - uint256(pos.collateral), Math.mulDiv(marketOracle.priceValue(), lltv_, 1e36), 1e18 - ); + uint256 debt = + (uint256(pos.borrowShares) * (uint256(mkt.totalBorrowAssets) + 1)) / (uint256(mkt.totalBorrowShares) + 1e6); + uint256 maxBorrow = + Math.mulDiv(uint256(pos.collateral), Math.mulDiv(marketOracle.priceValue(), lltv_, 1e36), 1e18); return Math.mulDiv(maxBorrow, 1e18, debt); } @@ -1673,12 +1644,8 @@ contract FCMVaultTest is Test { vault.rebalance(); - assertApproxEqAbs( - WETH.balanceOf(address(MORPHO)), collBefore, 1, "collateral unchanged (below band)" - ); - assertApproxEqAbs( - FUSDEV.balanceOf(address(vault)), yieldBefore, 1, "yield unchanged (below band)" - ); + assertApproxEqAbs(WETH.balanceOf(address(MORPHO)), collBefore, 1, "collateral unchanged (below band)"); + assertApproxEqAbs(FUSDEV.balanceOf(address(vault)), yieldBefore, 1, "yield unchanged (below band)"); } /// @notice A dust surplus whose swap output rounds to zero must no-op the harvest, @@ -1750,12 +1717,8 @@ contract FCMVaultTest is Test { vault.rebalance(); // does not revert while a recovery is merely pending - assertApproxEqAbs( - WETH.balanceOf(address(MORPHO)), collBefore, 1, "harvest frozen: no collateral" - ); - assertApproxEqAbs( - FUSDEV.balanceOf(address(vault)), yieldBefore, 1, "harvest frozen: yield untouched" - ); + assertApproxEqAbs(WETH.balanceOf(address(MORPHO)), collBefore, 1, "harvest frozen: no collateral"); + assertApproxEqAbs(FUSDEV.balanceOf(address(vault)), yieldBefore, 1, "harvest frozen: yield untouched"); } /// @notice With partial-fill swaps (#72), harvest can no longer block the delever: when @@ -1826,9 +1789,7 @@ contract FCMVaultTest is Test { uint256 feeShares = vault.balanceOf(feeRcpt); assertGt(feeShares, 0, "fee shares minted"); - assertApproxEqRel( - vault.convertToAssets(feeShares), nav * 200 / 10_000, 2e16, "mgmt ~2% NAV" - ); + assertApproxEqRel(vault.convertToAssets(feeShares), nav * 200 / 10_000, 2e16, "mgmt ~2% NAV"); } /// @notice Performance fee ~ rate * gain above HWM; not double-charged. @@ -1846,9 +1807,7 @@ contract FCMVaultTest is Test { vault.accrueFees(); uint256 feeShares = vault.balanceOf(feeRcpt); assertGt(feeShares, 0, "perf fee minted"); - assertApproxEqRel( - vault.convertToAssets(feeShares), gain * 2_000 / 10_000, 3e16, "perf ~20% gain" - ); + assertApproxEqRel(vault.convertToAssets(feeShares), gain * 2_000 / 10_000, 3e16, "perf ~20% gain"); // Second accrual with no new gain -> no additional fee (HWM holds). uint256 prev = vault.balanceOf(feeRcpt); @@ -2000,10 +1959,7 @@ contract FCMVaultTest is Test { vault.accrueFees(); assertApproxEqRel( - vault.convertToAssets(vault.balanceOf(feeRcpt)), - nav * 200 / 10_000, - 2e16, - "one year billed, not three" + vault.convertToAssets(vault.balanceOf(feeRcpt)), nav * 200 / 10_000, 2e16, "one year billed, not three" ); } @@ -2032,9 +1988,7 @@ contract FCMVaultTest is Test { // Tight tolerance: a naive (non-grossed) mint would land ~9.09%, far outside. uint256 recipientValue = vault.convertToAssets(vault.balanceOf(feeRcpt)); - assertApproxEqRel( - recipientValue, vault.totalAssets() / 10, 1e15, "recipient holds true 10%" - ); + assertApproxEqRel(recipientValue, vault.totalAssets() / 10, 1e15, "recipient holds true 10%"); } /// @notice Fees accrue on the `rebalance` path — the hook runs before the diff --git a/solidity/test/TvlLimit.t.sol b/solidity/test/TvlLimit.t.sol index 61605de..94d2713 100644 --- a/solidity/test/TvlLimit.t.sol +++ b/solidity/test/TvlLimit.t.sol @@ -125,10 +125,7 @@ contract TvlLimitTest is Test { function _expectMaxDepositExceeded(address receiver, uint256 assets) internal { vm.expectRevert( abi.encodeWithSelector( - ERC4626.ERC4626ExceededMaxDeposit.selector, - receiver, - assets, - vault.maxDeposit(receiver) + ERC4626.ERC4626ExceededMaxDeposit.selector, receiver, assets, vault.maxDeposit(receiver) ) ); } diff --git a/solidity/test/mocks/MockCpmmSwapRouter.sol b/solidity/test/mocks/MockCpmmSwapRouter.sol index e5acf86..d045460 100644 --- a/solidity/test/mocks/MockCpmmSwapRouter.sol +++ b/solidity/test/mocks/MockCpmmSwapRouter.sol @@ -39,8 +39,7 @@ contract MockCpmmSwapRouter { require(p.amountOutMinimum == 0, "amountOutMinimum not implemented in this mock"); bool zeroForOne = p.tokenIn < p.tokenOut; - (address token0, address token1) = - zeroForOne ? (p.tokenIn, p.tokenOut) : (p.tokenOut, p.tokenIn); + (address token0, address token1) = zeroForOne ? (p.tokenIn, p.tokenOut) : (p.tokenOut, p.tokenIn); uint256 r0 = reserveOf[token0]; uint256 r1 = reserveOf[token1]; require(r0 > 0 && r1 > 0, "reserves unset"); @@ -62,9 +61,7 @@ contract MockCpmmSwapRouter { if (consumed == 0) return 0; // out = reserveOut - k / (reserveIn + consumed) - amountOut = zeroForOne - ? r1 - Math.mulDiv(r0, r1, r0 + consumed) - : r0 - Math.mulDiv(r0, r1, r1 + consumed); + amountOut = zeroForOne ? r1 - Math.mulDiv(r0, r1, r0 + consumed) : r0 - Math.mulDiv(r0, r1, r1 + consumed); MockERC20(p.tokenIn).burn(msg.sender, consumed); MockERC20(p.tokenOut).mint(p.recipient, amountOut); diff --git a/solidity/test/mocks/MockMorpho.sol b/solidity/test/mocks/MockMorpho.sol index d908f77..57bb93e 100644 --- a/solidity/test/mocks/MockMorpho.sol +++ b/solidity/test/mocks/MockMorpho.sol @@ -25,12 +25,7 @@ contract MockMorpho { market[mp.id()].lastUpdate = uint128(block.timestamp); } - function supplyCollateral( - MarketParams memory mp, - uint256 assets, - address onBehalf, - bytes calldata - ) external { + function supplyCollateral(MarketParams memory mp, uint256 assets, address onBehalf, bytes calldata) external { require(assets != 0, "ZERO_ASSETS"); Id id = mp.id(); position[id][onBehalf].collateral += uint128(assets); @@ -44,14 +39,15 @@ contract MockMorpho { /*shares*/ address onBehalf, address receiver - ) external returns (uint256, uint256) { + ) + external + returns (uint256, uint256) + { Id id = mp.id(); Market storage m = market[id]; uint256 newShares = _mulDivUp( - assets, - uint256(m.totalBorrowShares) + VIRTUAL_SHARES, - uint256(m.totalBorrowAssets) + VIRTUAL_ASSETS + assets, uint256(m.totalBorrowShares) + VIRTUAL_SHARES, uint256(m.totalBorrowAssets) + VIRTUAL_ASSETS ); position[id][onBehalf].borrowShares += uint128(newShares); @@ -74,13 +70,10 @@ contract MockMorpho { /// mock-only safeguard against rounding overshoot on full repay; /// real Morpho enforces this via its accounting invariants. The /// `shares` and `data` parameters are ignored. - function repay( - MarketParams memory mp, - uint256 assets, - uint256 shares, - address onBehalf, - bytes calldata - ) external returns (uint256, uint256) { + function repay(MarketParams memory mp, uint256 assets, uint256 shares, address onBehalf, bytes calldata) + external + returns (uint256, uint256) + { Id id = mp.id(); Market storage m = market[id]; @@ -90,16 +83,12 @@ contract MockMorpho { if (shares > 0) { sharesToBurn = shares; assets = _mulDivUp( - shares, - uint256(m.totalBorrowAssets) + VIRTUAL_ASSETS, - uint256(m.totalBorrowShares) + VIRTUAL_SHARES + shares, uint256(m.totalBorrowAssets) + VIRTUAL_ASSETS, uint256(m.totalBorrowShares) + VIRTUAL_SHARES ); } else { // Morpho rounds by-assets repay DOWN (toSharesDown). sharesToBurn = _mulDivDown( - assets, - uint256(m.totalBorrowShares) + VIRTUAL_SHARES, - uint256(m.totalBorrowAssets) + VIRTUAL_ASSETS + assets, uint256(m.totalBorrowShares) + VIRTUAL_SHARES, uint256(m.totalBorrowAssets) + VIRTUAL_ASSETS ); } // No clamp: like Morpho, over-burning more shares than the position holds @@ -123,12 +112,7 @@ contract MockMorpho { /// needing HF-bound behavior should drive position state /// explicitly. Redeem tests don't need it because they repay /// debt before withdrawing collateral. - function withdrawCollateral( - MarketParams memory mp, - uint256 assets, - address onBehalf, - address receiver - ) external { + function withdrawCollateral(MarketParams memory mp, uint256 assets, address onBehalf, address receiver) external { Id id = mp.id(); position[id][onBehalf].collateral -= uint128(assets); IERC20(mp.collateralToken).transfer(receiver, assets); @@ -153,12 +137,9 @@ contract MockMorpho { /// @param seizedCollateral Collateral units removed from the position. /// @param repaidAssets Loan-token debt repaid (reduces borrow shares /// and market totals). - function liquidate( - MarketParams memory mp, - address borrower, - uint256 seizedCollateral, - uint256 repaidAssets - ) external { + function liquidate(MarketParams memory mp, address borrower, uint256 seizedCollateral, uint256 repaidAssets) + external + { Id id = mp.id(); Market storage m = market[id];