From 00dce60a623067a90975772f6e8c08e2f11f2c25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A1zaro=20Ra=C3=BAl=20Iglesias=20Vera?= Date: Mon, 21 Jul 2025 12:12:09 +0200 Subject: [PATCH 01/25] feat: Initial solvency check --- src/core/EulerSwap.sol | 244 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 242 insertions(+), 2 deletions(-) diff --git a/src/core/EulerSwap.sol b/src/core/EulerSwap.sol index 5de1a1445..d75a517e0 100644 --- a/src/core/EulerSwap.sol +++ b/src/core/EulerSwap.sol @@ -51,13 +51,50 @@ library FastEvc { mstore(0x2c, shl(0x60, account)) // clears `operator`'s padding mstore(0x0c, 0x1647292a000000000000000000000000) // selector for `isAccountOperatorAuthorized(address,address)` with `account`'s padding if iszero(staticcall(gas(), evc, 0x1c, 0x44, 0x00, 0x20)) { - returndatacopy(ptr, 0x00, returndatasize()) - revert(ptr, returndatasize()) + let ptr_ := mload(0x40) + returndatacopy(ptr_, 0x00, returndatasize()) + revert(ptr_, returndatasize()) } authorized := mload(0x00) mstore(0x40, ptr) } } + + function fastGetCollaterals(IEVC evc, address account) internal view returns (address[] memory collaterals) { + assembly ("memory-safe") { + mstore(0x14, account) + mstore(0x00, 0xa4d25d1e000000000000000000000000) // selector for `getCollaterals(address)` with `evc`'s padding + + if iszero(staticcall(gas(), evc, 0x00, 0x24, 0x00, 0x00)) { + let ptr := mload(0x40) + returndatacopy(ptr, 0x00, returndatasize()) + revert(ptr, returndatasize()) + } + + let size := returndatasize() + collaterals := mload(0x40) + returndatacopy(collaterals, 0x00, size) + mstore(0x40, add(collaterals, size)) + } + } + + function fastGetControllers(IEVC evc, address account) internal view returns (address[] memory controllers) { + assembly ("memory-safe") { + mstore(0x14, account) + mstore(0x00, 0xfd6046d7000000000000000000000000) // selector for `getControllers(address)` with `evc`'s padding + + if iszero(staticcall(gas(), evc, 0x00, 0x24, 0x00, 0x00)) { + let ptr := mload(0x40) + returndatacopy(ptr, 0x00, returndatasize()) + revert(ptr, returndatasize()) + } + + let size := returndatasize() + controllers := mload(0x40) + returndatacopy(controllers, 0x00, size) + mstore(0x40, add(controllers, size)) + } + } } interface IOracle { @@ -78,6 +115,26 @@ interface IOracle { // for computing collateral value, use `bidOutAmount` } +library FastOracle { + function fastGetQuote(IOracle oracle, uint256 inAmount, IERC20 base, IERC20 quote) internal view returns (uint256 bidOutAmount, uint256 askOutAmount) { + assembly ("memory-safe") { + let ptr := mload(0x40) + + mstore(ptr, 0x0902f1ac) // selector for `getQuotes(uint256,address,address)` + mstore(add(0x20, ptr), inAmount) + mstore(add(0x40, ptr), and(0xffffffffffffffffffffffffffffffffffffffff, base)) + mstore(add(0x60, ptr), and(0xffffffffffffffffffffffffffffffffffffffff, quote)) + if iszero(staticcall(gas(), oracle, 0x1c, 0x64, 0x00, 0x40)) { + let ptr_ := mload(0x40) + returndatacopy(ptr_, 0x00, returndatasize()) + revert(ptr_, returndatasize()) + } + bidOutAmount := mload(0x00) + askOutAmount := mload(0x20) + } + } +} + interface IEVault is IERC4626 { /// @notice Sum of all outstanding debts, in underlying units (increases as interest is accrued) /// @return The total borrows in asset units @@ -214,6 +271,47 @@ library FastEvault { if or(gt(0x40, returndatasize()), or(shr(0x10, supplyCap), shr(0x10, borrowCap))) { revert(0x00, 0x00) } } } + + function fastOracle(IEVault vault) internal view returns (IOracle oracle) { + assembly ("memory-safe") { + mstore(0x00, 0x7dc0d1d0) // selector for `oracle()` + if iszero(staticcall(gas(), vault, 0x1c, 0x04, 0x00, 0x20)) { + let ptr := mload(0x40) + returndatacopy(ptr, 0x00, returndatasize()) + revert(ptr, returndatasize()) + } + oracle := mload(0x00) + if or(gt(0x20, returndatasize()), shr(0xa0, oracle)) { revert(0x00, 0x00) } + } + } + + function fastUnitOfAccount(IEVault vault) internal view returns (IERC20 unitOfAccount) { + assembly ("memory-safe") { + mstore(0x00, 0x3e833364) // selector for `unitOfAccount()` + if iszero(staticcall(gas(), vault, 0x1c, 0x04, 0x00, 0x20)) { + let ptr := mload(0x40) + returndatacopy(ptr, 0x00, returndatasize()) + revert(ptr, returndatasize()) + } + unitOfAccount := mload(0x00) + if or(gt(0x20, returndatasize()), shr(0xa0, unitOfAccount)) { revert(0x00, 0x00) } + } + } + + // LTV is returned as `uint256` for efficiency, but they are checked to ensure that they do not overflow a `uint16`. + function fastLTVBorrow(IEVault vault, IEVault collateral) internal view returns (uint256 ltv) { + assembly ("memory-safe") { + mstore(0x14, collateral) + mstore(0x00, 0xbf58094d000000000000000000000000) // selector for `LTVBorrow(address)` with `collateral`'s padding + if iszero(staticcall(gas(), vault, 0x10, 0x24, 0x00, 0x20)) { + let ptr := mload(0x40) + returndatacopy(ptr, 0x00, returndatasize()) + revert(ptr, returndatasize()) + } + ltv := mload(0x00) + if or(gt(0x20, returndatasize()), shr(0x10, ltv)) { revert(0x00, 0x00) } + } + } } interface IEulerSwap { @@ -392,6 +490,7 @@ abstract contract EulerSwap is SettlerAbstract { using FastEvc for IEVC; using FastEvault for IEVault; using FastEulerSwap for IEulerSwap; + using FastOracle for IOracle; function _EVC() internal view virtual returns (IEVC); @@ -429,6 +528,9 @@ abstract contract EulerSwap is SettlerAbstract { unchecked { sellAmount = sellToken.fastBalanceOf(address(this)) * bps / BASIS; } + // TODO: Comment for what happens with excess here. + // 1. Excess is absorbed by other sources + // 2. Ultimatelly donated as fee, which might result in a slippage revert? Double check. sellAmount = (sellAmount > inLimit).ternary(inLimit, sellAmount); sellToken.safeTransfer(address(pool), sellAmount); } @@ -592,4 +694,142 @@ abstract contract EulerSwap is SettlerAbstract { return (amountCap == 0).ternary(type(uint112).max, 10 ** (amountCap & 63) * (amountCap >> 6) / 100); } } + + function checkEulerSwapSolvencyAfterSwap(IEulerSwap pool, bool zeroForOne, uint256 amount) external view returns (uint256) { + ParamsLib.Params p = pool.fastGetParams(); + + // Check it is not over limit + (uint256 reserve0, uint256 reserve1) = pool.fastGetReserves(); + { + (uint256 inLimit,) = calcLimits(pool, zeroForOne, p, reserve0, reserve1); + + if(amount > inLimit) return 0; + } + + // Check reserves after the swap + uint256 amountOut = findCurvePoint(amount, zeroForOne, p, reserve0, reserve1); + { + uint256 newReserve0 = reserve0; + uint256 newReserve1 = reserve1; + + if(zeroForOne) { + newReserve0 += amount; + newReserve1 -= amountOut; + } else { + newReserve0 -= amountOut; + newReserve1 += amount; + } + + if(!CurveLib.verify( + newReserve0, + newReserve1, + p.equilibriumReserve0(), + p.equilibriumReserve1(), + p.priceX(), p.priceY(), + p.concentrationX(), + p.concentrationY() + )) { + return 0; + } + } + + // Check solvency + { + address account = p.eulerAccount(); + address[] memory collaterals; + IEVault debtVault; + + { + IEVC evc = _EVC(); + collaterals = evc.fastGetCollaterals(account); + address[] memory controllers = evc.fastGetControllers(account); + + if(controllers.length > 1) return 0; + if(controllers.length == 1) debtVault = IEVault(controllers[0]); + } + + IEVault sellVault; + IEVault buyVault; + { + (address sellVault_, address buyVault_) = zeroForOne.maybeSwap(address(p.vault1()), address(p.vault0())); + sellVault = IEVault(sellVault_); + buyVault = IEVault(buyVault_); + } + + uint256 debt; + uint256 newDebt; + uint256 soldCollateral; + uint256 newCollateral; + + // compute new debt in buyVault + { + uint256 collateralBalance = buyVault.fastBalanceOf(account); + if(collateralBalance < amountOut) { + debt = amountOut - collateralBalance; + soldCollateral = collateralBalance; + } + else { + soldCollateral = amountOut; + } + } + + // check sellVault debt + if(debtVault == sellVault) { + // debt repayment + uint256 prevDebt = sellVault.fastDebtOf(account); + if(amount < prevDebt) { + // collateral in buyVault should be able to cover the amountOut + if(newDebt != 0) return 0; + + debt = prevDebt - amount; + } + else { + newCollateral = amount - prevDebt; + debtVault = buyVault; + debt = newDebt; + } + } else { + newCollateral = amount; + } + + // if there is new debt in buyVault, check controllers + // needs to be buyVault to ensure there is only one controller at the end + if(newDebt != 0 && debtVault != buyVault) { + return 0; + } + + // check for solvency if there is any debt + if(debt != 0) { + IOracle oracle = debtVault.fastOracle(); + IERC20 unitOfAccount = debtVault.fastUnitOfAccount(); + + (, debt) = oracle.fastGetQuote(debt, debtVault.fastAsset(), unitOfAccount); + // adjust precision to avoid divisions when adjusting LTVBorrow bps of collateral. + // All amounts in EulerSwap are lower than uint112 so there should not be overflow errors. + debt *= 1e4; + if(soldCollateral > 0) { + // adjust inequality to avoid substraction + // collateral + newCollateral - soldCollateral >= debt + // is changed to + // collateral + newCollateral >= debt + soldCollateral + debt += (soldCollateral * debtVault.fastLTVBorrow(buyVault)); + } + uint256 collateral; + if(newCollateral != 0) { + collateral = (newCollateral * debtVault.fastLTVBorrow(sellVault)); + } + for(uint256 i = 0; i < collaterals.length; i++) { + IEVault collateralVault = IEVault(collaterals[i]); + (uint256 value,) = oracle.fastGetQuote(collateralVault.fastBalanceOf(account), collateralVault.fastAsset(), unitOfAccount); + + collateral += (value * debtVault.fastLTVBorrow(collateralVault)); + if(collateral >= debt) { + return amountOut; + } + } + } + } + + return 0; + } } From 7075631dbc89ac66220d9ef08add9c94cd19d7b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A1zaro=20Ra=C3=BAl=20Iglesias=20Vera?= Date: Mon, 21 Jul 2025 12:45:40 +0200 Subject: [PATCH 02/25] chore: Minor fixes/updates --- src/core/EulerSwap.sol | 84 +++++++++++++++++++++++------------------- 1 file changed, 47 insertions(+), 37 deletions(-) diff --git a/src/core/EulerSwap.sol b/src/core/EulerSwap.sol index d75a517e0..4f4a05fc5 100644 --- a/src/core/EulerSwap.sol +++ b/src/core/EulerSwap.sol @@ -698,38 +698,42 @@ abstract contract EulerSwap is SettlerAbstract { function checkEulerSwapSolvencyAfterSwap(IEulerSwap pool, bool zeroForOne, uint256 amount) external view returns (uint256) { ParamsLib.Params p = pool.fastGetParams(); - // Check it is not over limit - (uint256 reserve0, uint256 reserve1) = pool.fastGetReserves(); - { - (uint256 inLimit,) = calcLimits(pool, zeroForOne, p, reserve0, reserve1); - - if(amount > inLimit) return 0; - } - - // Check reserves after the swap - uint256 amountOut = findCurvePoint(amount, zeroForOne, p, reserve0, reserve1); + uint256 amountOut; + // pre-swap checks { - uint256 newReserve0 = reserve0; - uint256 newReserve1 = reserve1; + // Check it is not over limit + (uint256 reserve0, uint256 reserve1) = pool.fastGetReserves(); + { + (uint256 inLimit,) = calcLimits(pool, zeroForOne, p, reserve0, reserve1); - if(zeroForOne) { - newReserve0 += amount; - newReserve1 -= amountOut; - } else { - newReserve0 -= amountOut; - newReserve1 += amount; + if(amount > inLimit) return 0; } - if(!CurveLib.verify( - newReserve0, - newReserve1, - p.equilibriumReserve0(), - p.equilibriumReserve1(), - p.priceX(), p.priceY(), - p.concentrationX(), - p.concentrationY() - )) { - return 0; + // Check reserves after the swap + amountOut = findCurvePoint(amount, zeroForOne, p, reserve0, reserve1); + { + uint256 newReserve0 = reserve0; + uint256 newReserve1 = reserve1; + + if(zeroForOne) { + newReserve0 += amount; + newReserve1 -= amountOut; + } else { + newReserve0 -= amountOut; + newReserve1 += amount; + } + + if(!CurveLib.verify( + newReserve0, + newReserve1, + p.equilibriumReserve0(), + p.equilibriumReserve1(), + p.priceX(), p.priceY(), + p.concentrationX(), + p.concentrationY() + )) { + return 0; + } } } @@ -765,7 +769,7 @@ abstract contract EulerSwap is SettlerAbstract { { uint256 collateralBalance = buyVault.fastBalanceOf(account); if(collateralBalance < amountOut) { - debt = amountOut - collateralBalance; + newDebt = amountOut - collateralBalance; soldCollateral = collateralBalance; } else { @@ -779,25 +783,31 @@ abstract contract EulerSwap is SettlerAbstract { uint256 prevDebt = sellVault.fastDebtOf(account); if(amount < prevDebt) { // collateral in buyVault should be able to cover the amountOut + // to ensure there is only one controller at the end if(newDebt != 0) return 0; - debt = prevDebt - amount; } else { newCollateral = amount - prevDebt; - debtVault = buyVault; - debt = newDebt; + // debt was repaid, no controller needed unless there is new debt + debtVault = IEVault(address(0)); } } else { newCollateral = amount; } - // if there is new debt in buyVault, check controllers - // needs to be buyVault to ensure there is only one controller at the end - if(newDebt != 0 && debtVault != buyVault) { - return 0; + if(newDebt != 0) { + if(address(debtVault) == address(0)) { + debtVault = buyVault; + } + else { + // if there is new debt in buyVault, check controllers + // needs to be buyVault to ensure there is only one controller at the end + if(debtVault != buyVault) return 0; + } + debt = newDebt; } - + // check for solvency if there is any debt if(debt != 0) { IOracle oracle = debtVault.fastOracle(); From 22e3117fc7d8122421653cd2b653d62d720f867b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A1zaro=20Ra=C3=BAl=20Iglesias=20Vera?= Date: Mon, 21 Jul 2025 13:12:19 +0200 Subject: [PATCH 03/25] chore: Remove Solvency checks from EulerSwap base contract --- src/core/EulerSwap.sol | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/core/EulerSwap.sol b/src/core/EulerSwap.sol index 4f4a05fc5..0a89aac65 100644 --- a/src/core/EulerSwap.sol +++ b/src/core/EulerSwap.sol @@ -490,7 +490,6 @@ abstract contract EulerSwap is SettlerAbstract { using FastEvc for IEVC; using FastEvault for IEVault; using FastEulerSwap for IEulerSwap; - using FastOracle for IOracle; function _EVC() internal view virtual returns (IEVC); @@ -560,7 +559,7 @@ abstract contract EulerSwap is SettlerAbstract { } function findCurvePoint(uint256 amount, bool zeroForOne, ParamsLib.Params p, uint256 reserve0, uint256 reserve1) - private + internal pure returns (uint256) { @@ -606,7 +605,7 @@ abstract contract EulerSwap is SettlerAbstract { /// @return inLimit Maximum amount of input token that can be deposited /// @return outLimit Maximum amount of output token that can be withdrawn function calcLimits(IEulerSwap pool, bool zeroForOne, ParamsLib.Params p, uint256 reserve0, uint256 reserve1) - private + internal view returns (uint256 inLimit, uint256 outLimit) { @@ -694,6 +693,17 @@ abstract contract EulerSwap is SettlerAbstract { return (amountCap == 0).ternary(type(uint112).max, 10 ** (amountCap & 63) * (amountCap >> 6) / 100); } } +} + +abstract contract EulerSwapExtended is EulerSwap { + using Ternary for bool; + using SafeTransferLib for IEVault; + using ParamsLib for ParamsLib.Params; + using ParamsLib for IEulerSwap; + using FastEvc for IEVC; + using FastEvault for IEVault; + using FastEulerSwap for IEulerSwap; + using FastOracle for IOracle; function checkEulerSwapSolvencyAfterSwap(IEulerSwap pool, bool zeroForOne, uint256 amount) external view returns (uint256) { ParamsLib.Params p = pool.fastGetParams(); @@ -777,6 +787,8 @@ abstract contract EulerSwap is SettlerAbstract { } } + debt; + // check sellVault debt if(debtVault == sellVault) { // debt repayment @@ -842,4 +854,4 @@ abstract contract EulerSwap is SettlerAbstract { return 0; } -} +} \ No newline at end of file From e0799bdec0d5c9189e12494cb7f9aff0d4847195 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A1zaro=20Ra=C3=BAl=20Iglesias=20Vera?= Date: Mon, 21 Jul 2025 17:37:38 +0200 Subject: [PATCH 04/25] fix: collateral and debt calculation --- src/core/EulerSwap.sol | 221 +++++++++++++++++------------------------ 1 file changed, 90 insertions(+), 131 deletions(-) diff --git a/src/core/EulerSwap.sol b/src/core/EulerSwap.sol index 0a89aac65..5db28d7ca 100644 --- a/src/core/EulerSwap.sol +++ b/src/core/EulerSwap.sol @@ -705,153 +705,112 @@ abstract contract EulerSwapExtended is EulerSwap { using FastEulerSwap for IEulerSwap; using FastOracle for IOracle; - function checkEulerSwapSolvencyAfterSwap(IEulerSwap pool, bool zeroForOne, uint256 amount) external view returns (uint256) { - ParamsLib.Params p = pool.fastGetParams(); + function checkSolvency( + address account, + address vault0, + address vault1, + bool zeroForOne, + uint256 amountIn, + uint256 amountOut + ) external view returns (bool) { + address[] memory collaterals; + IEVault debtVault; + uint256 debt; - uint256 amountOut; - // pre-swap checks { - // Check it is not over limit - (uint256 reserve0, uint256 reserve1) = pool.fastGetReserves(); - { - (uint256 inLimit,) = calcLimits(pool, zeroForOne, p, reserve0, reserve1); - - if(amount > inLimit) return 0; - } - - // Check reserves after the swap - amountOut = findCurvePoint(amount, zeroForOne, p, reserve0, reserve1); - { - uint256 newReserve0 = reserve0; - uint256 newReserve1 = reserve1; - - if(zeroForOne) { - newReserve0 += amount; - newReserve1 -= amountOut; - } else { - newReserve0 -= amountOut; - newReserve1 += amount; - } - - if(!CurveLib.verify( - newReserve0, - newReserve1, - p.equilibriumReserve0(), - p.equilibriumReserve1(), - p.priceX(), p.priceY(), - p.concentrationX(), - p.concentrationY() - )) { - return 0; - } + IEVC evc = _EVC(); + collaterals = evc.fastGetCollaterals(account); + address[] memory controllers = evc.fastGetControllers(account); + + if(controllers.length > 1) return false; + if(controllers.length == 1) { + debtVault = IEVault(controllers[0]); + debt = debtVault.fastDebtOf(account); } } - // Check solvency + IEVault sellVault; + IEVault buyVault; { - address account = p.eulerAccount(); - address[] memory collaterals; - IEVault debtVault; - - { - IEVC evc = _EVC(); - collaterals = evc.fastGetCollaterals(account); - address[] memory controllers = evc.fastGetControllers(account); + (address sellVault_, address buyVault_) = zeroForOne.maybeSwap(vault1, vault0); + sellVault = IEVault(sellVault_); + buyVault = IEVault(buyVault_); + } + + uint256 newDebt; + uint256 soldCollateral; + uint256 newCollateral; - if(controllers.length > 1) return 0; - if(controllers.length == 1) debtVault = IEVault(controllers[0]); + // compute new debt in buyVault + { + uint256 collateralBalance = buyVault.fastBalanceOf(account); + if(collateralBalance < amountOut) { + newDebt = amountOut - collateralBalance; + soldCollateral = collateralBalance; } - - IEVault sellVault; - IEVault buyVault; - { - (address sellVault_, address buyVault_) = zeroForOne.maybeSwap(address(p.vault1()), address(p.vault0())); - sellVault = IEVault(sellVault_); - buyVault = IEVault(buyVault_); + else { + soldCollateral = amountOut; } - - uint256 debt; - uint256 newDebt; - uint256 soldCollateral; - uint256 newCollateral; + } - // compute new debt in buyVault - { - uint256 collateralBalance = buyVault.fastBalanceOf(account); - if(collateralBalance < amountOut) { - newDebt = amountOut - collateralBalance; - soldCollateral = collateralBalance; - } - else { - soldCollateral = amountOut; - } + // check sellVault debt + if(debtVault == sellVault) { + // debt repayment + if(amountIn < debt) { + // collateral in buyVault must be able to cover the amountOut + // to ensure there is only one controller at the end + if(newDebt != 0) return false; + debt -= amountIn; } - - debt; - - // check sellVault debt - if(debtVault == sellVault) { - // debt repayment - uint256 prevDebt = sellVault.fastDebtOf(account); - if(amount < prevDebt) { - // collateral in buyVault should be able to cover the amountOut - // to ensure there is only one controller at the end - if(newDebt != 0) return 0; - debt = prevDebt - amount; - } - else { - newCollateral = amount - prevDebt; - // debt was repaid, no controller needed unless there is new debt - debtVault = IEVault(address(0)); - } - } else { - newCollateral = amount; + else { + newCollateral = amountIn - debt; + debt = 0; // debt was repaid } - - if(newDebt != 0) { - if(address(debtVault) == address(0)) { - debtVault = buyVault; - } - else { - // if there is new debt in buyVault, check controllers - // needs to be buyVault to ensure there is only one controller at the end - if(debtVault != buyVault) return 0; - } - debt = newDebt; + } else { + newCollateral = amountIn; + } + + // if there is new debt in buyVault, check that the controller is buyVault + // or debt was repaid to ensure there is only one controller at the end + if(newDebt != 0) { + if(debt != 0 && debtVault != buyVault) return false; + if(address(debtVault) == address(0)) debtVault = buyVault; + debt += newDebt; + } + + // check for solvency if there is any debt + if(debt != 0) { + IOracle oracle = debtVault.fastOracle(); + IERC20 unitOfAccount = debtVault.fastUnitOfAccount(); + + (, debt) = oracle.fastGetQuote(debt, debtVault.fastAsset(), unitOfAccount); + // adjust precision to avoid divisions when adjusting LTVBorrow bps of collateral. + // All amounts in EulerSwap are lower than uint112 so there should not be overflow errors. + debt *= 1e4; + if(soldCollateral != 0) { + // adjust inequality to avoid substraction + // collateral + newCollateral - soldCollateral >= debt + // is changed to + // collateral + newCollateral >= debt + soldCollateral + (uint256 value,) = oracle.fastGetQuote(soldCollateral, buyVault.fastAsset(), unitOfAccount); + debt += (value * debtVault.fastLTVBorrow(buyVault)); } - - // check for solvency if there is any debt - if(debt != 0) { - IOracle oracle = debtVault.fastOracle(); - IERC20 unitOfAccount = debtVault.fastUnitOfAccount(); - - (, debt) = oracle.fastGetQuote(debt, debtVault.fastAsset(), unitOfAccount); - // adjust precision to avoid divisions when adjusting LTVBorrow bps of collateral. - // All amounts in EulerSwap are lower than uint112 so there should not be overflow errors. - debt *= 1e4; - if(soldCollateral > 0) { - // adjust inequality to avoid substraction - // collateral + newCollateral - soldCollateral >= debt - // is changed to - // collateral + newCollateral >= debt + soldCollateral - debt += (soldCollateral * debtVault.fastLTVBorrow(buyVault)); - } - uint256 collateral; - if(newCollateral != 0) { - collateral = (newCollateral * debtVault.fastLTVBorrow(sellVault)); - } - for(uint256 i = 0; i < collaterals.length; i++) { - IEVault collateralVault = IEVault(collaterals[i]); - (uint256 value,) = oracle.fastGetQuote(collateralVault.fastBalanceOf(account), collateralVault.fastAsset(), unitOfAccount); - - collateral += (value * debtVault.fastLTVBorrow(collateralVault)); - if(collateral >= debt) { - return amountOut; - } + uint256 collateral; + if(newCollateral != 0) { + (uint256 value,) = oracle.fastGetQuote(newCollateral, sellVault.fastAsset(), unitOfAccount); + collateral = (value * debtVault.fastLTVBorrow(sellVault)); + } + for(uint256 i = 0; i < collaterals.length; i++) { + IEVault collateralVault = IEVault(collaterals[i]); + (uint256 value,) = oracle.fastGetQuote(collateralVault.fastBalanceOf(account), collateralVault.fastAsset(), unitOfAccount); + + collateral += (value * debtVault.fastLTVBorrow(collateralVault)); + if(collateral >= debt) { + return true; } } } - return 0; + return false; } } \ No newline at end of file From 2240c58bc1905c0fe5faf1518d51c4a1e4d5238c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A1zaro=20Ra=C3=BAl=20Iglesias=20Vera?= Date: Mon, 21 Jul 2025 18:15:54 +0200 Subject: [PATCH 05/25] chore: Move solvency calculations into a library --- src/core/EulerSwap.sol | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/core/EulerSwap.sol b/src/core/EulerSwap.sol index 5db28d7ca..131337b37 100644 --- a/src/core/EulerSwap.sol +++ b/src/core/EulerSwap.sol @@ -695,7 +695,7 @@ abstract contract EulerSwap is SettlerAbstract { } } -abstract contract EulerSwapExtended is EulerSwap { +library EulerSwapSolvency { using Ternary for bool; using SafeTransferLib for IEVault; using ParamsLib for ParamsLib.Params; @@ -706,20 +706,19 @@ abstract contract EulerSwapExtended is EulerSwap { using FastOracle for IOracle; function checkSolvency( + IEVC evc, address account, address vault0, address vault1, bool zeroForOne, uint256 amountIn, uint256 amountOut - ) external view returns (bool) { - address[] memory collaterals; + ) internal view returns (bool) { + address[] memory collaterals = evc.fastGetCollaterals(account); IEVault debtVault; uint256 debt; { - IEVC evc = _EVC(); - collaterals = evc.fastGetCollaterals(account); address[] memory controllers = evc.fastGetControllers(account); if(controllers.length > 1) return false; From 4ad4019d54f730758f6350fbe063b4f2fdb3008f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A1zaro=20Ra=C3=BAl=20Iglesias=20Vera?= Date: Mon, 21 Jul 2025 18:24:14 +0200 Subject: [PATCH 06/25] chore: Move more functionalities into EulerSwapLib --- src/core/EulerSwap.sol | 169 ++++++++++++++++++++--------------------- 1 file changed, 82 insertions(+), 87 deletions(-) diff --git a/src/core/EulerSwap.sol b/src/core/EulerSwap.sol index 131337b37..851f2502d 100644 --- a/src/core/EulerSwap.sol +++ b/src/core/EulerSwap.sol @@ -478,85 +478,15 @@ library ParamsLib { } } -abstract contract EulerSwap is SettlerAbstract { - using FastLogic for bool; - using Ternary for bool; +library EulerSwapLib { using UnsafeMath for uint256; using Math for uint256; - using SafeTransferLib for IERC20; + using Ternary for bool; using SafeTransferLib for IEVault; using ParamsLib for ParamsLib.Params; - using ParamsLib for IEulerSwap; using FastEvc for IEVC; using FastEvault for IEVault; - using FastEulerSwap for IEulerSwap; - - function _EVC() internal view virtual returns (IEVC); - - function _revertTooMuchSlippage( - bool zeroForOne, - ParamsLib.Params p, - uint256 expectedBuyAmount, - uint256 actualBuyAmount - ) private view { - revertTooMuchSlippage( - IEVault(zeroForOne.ternary(address(p.vault1()), address(p.vault0()))).fastAsset(), - expectedBuyAmount, - actualBuyAmount - ); - } - - function sellToEulerSwap( - address recipient, - IERC20 sellToken, - uint256 bps, - IEulerSwap pool, - bool zeroForOne, - uint256 amountOutMin - ) internal { - // Doing this first violates the general rule that we ought to interact with the token - // before checking the state of the pool. However, this is safe because Euler doesn't admit - // badly-behaved tokens, and a token must be available on Euler before it can be added to - // EulerSwap. - ParamsLib.Params p = pool.fastGetParams(); - (uint256 reserve0, uint256 reserve1) = pool.fastGetReserves(); - (uint256 inLimit,) = calcLimits(pool, zeroForOne, p, reserve0, reserve1); - - uint256 sellAmount; - if (bps != 0) { - unchecked { - sellAmount = sellToken.fastBalanceOf(address(this)) * bps / BASIS; - } - // TODO: Comment for what happens with excess here. - // 1. Excess is absorbed by other sources - // 2. Ultimatelly donated as fee, which might result in a slippage revert? Double check. - sellAmount = (sellAmount > inLimit).ternary(inLimit, sellAmount); - sellToken.safeTransfer(address(pool), sellAmount); - } - if (sellAmount == 0) { - sellAmount = sellToken.fastBalanceOf(address(pool)); - // If the sell amount is over the limit, the excess is donated. Obviously, this may - // result in a slippage revert. - sellAmount = (sellAmount > inLimit).ternary(inLimit, sellAmount); - } - - // solve the constant function - uint256 amountOut = findCurvePoint(sellAmount, zeroForOne, p, reserve0, reserve1); - - // check slippage before swapping to save some sad-path gas - if (amountOut < amountOutMin) { - _revertTooMuchSlippage(zeroForOne, p, amountOutMin, amountOut); - } - - // Because the reference implementation of `verify` for the EulerSwap trading function is - // non-monotonic, it may be possible to have an `amountOut` of one, even if `sellAmount` is - // zero. Because this is likely triggered by a failure of the `isAccountOperatorAuthorized` - // check, we skip calling `swap` because it's probably going to revert. If you set - // `amountOutMin` to one and this catches you off guard, I'm sorry, but that was dumb. - if (amountOut > 1) { - pool.fastSwap(zeroForOne, amountOut, recipient); - } - } + using FastOracle for IOracle; function findCurvePoint(uint256 amount, bool zeroForOne, ParamsLib.Params p, uint256 reserve0, uint256 reserve1) internal @@ -604,7 +534,7 @@ abstract contract EulerSwap is SettlerAbstract { /// @param zeroForOne Boolean indicating whether asset0 (true) or asset1 (false) is the input token /// @return inLimit Maximum amount of input token that can be deposited /// @return outLimit Maximum amount of output token that can be withdrawn - function calcLimits(IEulerSwap pool, bool zeroForOne, ParamsLib.Params p, uint256 reserve0, uint256 reserve1) + function calcLimits(IEVC evc, IEulerSwap pool, bool zeroForOne, ParamsLib.Params p, uint256 reserve0, uint256 reserve1) internal view returns (uint256 inLimit, uint256 outLimit) @@ -621,7 +551,7 @@ abstract contract EulerSwap is SettlerAbstract { // Supply caps on input unchecked { inLimit = sellVault.fastDebtOf(ownerAccount) + sellVault.fastMaxDeposit(ownerAccount); - inLimit = _EVC().fastIsAccountOperatorAuthorized(ownerAccount, address(pool)).orZero(inLimit); + inLimit = evc.fastIsAccountOperatorAuthorized(ownerAccount, address(pool)).orZero(inLimit); } // Remaining reserves of output @@ -693,17 +623,6 @@ abstract contract EulerSwap is SettlerAbstract { return (amountCap == 0).ternary(type(uint112).max, 10 ** (amountCap & 63) * (amountCap >> 6) / 100); } } -} - -library EulerSwapSolvency { - using Ternary for bool; - using SafeTransferLib for IEVault; - using ParamsLib for ParamsLib.Params; - using ParamsLib for IEulerSwap; - using FastEvc for IEVC; - using FastEvault for IEVault; - using FastEulerSwap for IEulerSwap; - using FastOracle for IOracle; function checkSolvency( IEVC evc, @@ -812,4 +731,80 @@ library EulerSwapSolvency { return false; } -} \ No newline at end of file +} + +abstract contract EulerSwap is SettlerAbstract { + using Ternary for bool; + using SafeTransferLib for IERC20; + using ParamsLib for ParamsLib.Params; + using ParamsLib for IEulerSwap; + using FastEvault for IEVault; + using FastEulerSwap for IEulerSwap; + + function _EVC() internal view virtual returns (IEVC); + + function _revertTooMuchSlippage( + bool zeroForOne, + ParamsLib.Params p, + uint256 expectedBuyAmount, + uint256 actualBuyAmount + ) private view { + revertTooMuchSlippage( + IEVault(zeroForOne.ternary(address(p.vault1()), address(p.vault0()))).fastAsset(), + expectedBuyAmount, + actualBuyAmount + ); + } + + function sellToEulerSwap( + address recipient, + IERC20 sellToken, + uint256 bps, + IEulerSwap pool, + bool zeroForOne, + uint256 amountOutMin + ) internal { + // Doing this first violates the general rule that we ought to interact with the token + // before checking the state of the pool. However, this is safe because Euler doesn't admit + // badly-behaved tokens, and a token must be available on Euler before it can be added to + // EulerSwap. + ParamsLib.Params p = pool.fastGetParams(); + (uint256 reserve0, uint256 reserve1) = pool.fastGetReserves(); + (uint256 inLimit,) = EulerSwapLib.calcLimits(_EVC(), pool, zeroForOne, p, reserve0, reserve1); + + uint256 sellAmount; + if (bps != 0) { + unchecked { + sellAmount = sellToken.fastBalanceOf(address(this)) * bps / BASIS; + } + // TODO: Comment for what happens with excess here. + // 1. Excess is absorbed by other sources + // 2. Ultimatelly donated as fee, which might result in a slippage revert? Double check. + sellAmount = (sellAmount > inLimit).ternary(inLimit, sellAmount); + sellToken.safeTransfer(address(pool), sellAmount); + } + if (sellAmount == 0) { + sellAmount = sellToken.fastBalanceOf(address(pool)); + // If the sell amount is over the limit, the excess is donated. Obviously, this may + // result in a slippage revert. + sellAmount = (sellAmount > inLimit).ternary(inLimit, sellAmount); + } + + // solve the constant function + uint256 amountOut = EulerSwapLib.findCurvePoint(sellAmount, zeroForOne, p, reserve0, reserve1); + + // check slippage before swapping to save some sad-path gas + if (amountOut < amountOutMin) { + _revertTooMuchSlippage(zeroForOne, p, amountOutMin, amountOut); + } + + // Because the reference implementation of `verify` for the EulerSwap trading function is + // non-monotonic, it may be possible to have an `amountOut` of one, even if `sellAmount` is + // zero. Because this is likely triggered by a failure of the `isAccountOperatorAuthorized` + // check, we skip calling `swap` because it's probably going to revert. If you set + // `amountOutMin` to one and this catches you off guard, I'm sorry, but that was dumb. + if (amountOut > 1) { + pool.fastSwap(zeroForOne, amountOut, recipient); + } + } +} From 65bcd56f5a7fbaecefc3d2a72e6d3940aedf1652 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A1zaro=20Ra=C3=BAl=20Iglesias=20Vera?= Date: Tue, 22 Jul 2025 15:56:54 +0200 Subject: [PATCH 07/25] fix: assembly errors --- src/core/EulerSwap.sol | 17 ++++++++--------- test/integration/EulerSwap.t.sol | 25 ++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/src/core/EulerSwap.sol b/src/core/EulerSwap.sol index 851f2502d..946be468e 100644 --- a/src/core/EulerSwap.sol +++ b/src/core/EulerSwap.sol @@ -65,15 +65,15 @@ library FastEvc { mstore(0x14, account) mstore(0x00, 0xa4d25d1e000000000000000000000000) // selector for `getCollaterals(address)` with `evc`'s padding - if iszero(staticcall(gas(), evc, 0x00, 0x24, 0x00, 0x00)) { + if iszero(staticcall(gas(), evc, 0x10, 0x24, 0x00, 0x00)) { let ptr := mload(0x40) returndatacopy(ptr, 0x00, returndatasize()) revert(ptr, returndatasize()) } - let size := returndatasize() + let size := sub(returndatasize(), 0x20) collaterals := mload(0x40) - returndatacopy(collaterals, 0x00, size) + returndatacopy(collaterals, 0x20, size) mstore(0x40, add(collaterals, size)) } } @@ -83,15 +83,15 @@ library FastEvc { mstore(0x14, account) mstore(0x00, 0xfd6046d7000000000000000000000000) // selector for `getControllers(address)` with `evc`'s padding - if iszero(staticcall(gas(), evc, 0x00, 0x24, 0x00, 0x00)) { + if iszero(staticcall(gas(), evc, 0x10, 0x24, 0x00, 0x00)) { let ptr := mload(0x40) returndatacopy(ptr, 0x00, returndatasize()) revert(ptr, returndatasize()) } - let size := returndatasize() + let size := sub(returndatasize(), 0x20) controllers := mload(0x40) - returndatacopy(controllers, 0x00, size) + returndatacopy(controllers, 0x20, size) mstore(0x40, add(controllers, size)) } } @@ -120,11 +120,11 @@ library FastOracle { assembly ("memory-safe") { let ptr := mload(0x40) - mstore(ptr, 0x0902f1ac) // selector for `getQuotes(uint256,address,address)` + mstore(ptr, 0x0579e61f) // selector for `getQuotes(uint256,address,address)` mstore(add(0x20, ptr), inAmount) mstore(add(0x40, ptr), and(0xffffffffffffffffffffffffffffffffffffffff, base)) mstore(add(0x60, ptr), and(0xffffffffffffffffffffffffffffffffffffffff, quote)) - if iszero(staticcall(gas(), oracle, 0x1c, 0x64, 0x00, 0x40)) { + if iszero(staticcall(gas(), oracle, add(0x1c, ptr), 0x64, 0x00, 0x40)) { let ptr_ := mload(0x40) returndatacopy(ptr_, 0x00, returndatasize()) revert(ptr_, returndatasize()) @@ -701,7 +701,6 @@ library EulerSwapLib { IOracle oracle = debtVault.fastOracle(); IERC20 unitOfAccount = debtVault.fastUnitOfAccount(); - (, debt) = oracle.fastGetQuote(debt, debtVault.fastAsset(), unitOfAccount); // adjust precision to avoid divisions when adjusting LTVBorrow bps of collateral. // All amounts in EulerSwap are lower than uint112 so there should not be overflow errors. debt *= 1e4; diff --git a/test/integration/EulerSwap.t.sol b/test/integration/EulerSwap.t.sol index aa5c3f749..a8d6c0731 100644 --- a/test/integration/EulerSwap.t.sol +++ b/test/integration/EulerSwap.t.sol @@ -11,7 +11,7 @@ import {Settler} from "src/Settler.sol"; import {SafeTransferLib} from "src/vendor/SafeTransferLib.sol"; -import {IEVC, IEulerSwap} from "src/core/EulerSwap.sol"; +import {IEVC, IEulerSwap, EulerSwapLib, ParamsLib, FastEulerSwap} from "src/core/EulerSwap.sol"; import {AllowanceHolderPairTest} from "./AllowanceHolderPairTest.t.sol"; @@ -19,6 +19,9 @@ IEVC constant EVC = IEVC(0x0C9a3dd6b8F28529d72d7f9cE918D493519EE383); abstract contract EulerSwapTest is AllowanceHolderPairTest { using SafeTransferLib for IERC20; + using ParamsLib for IEulerSwap; + using ParamsLib for ParamsLib.Params; + using FastEulerSwap for IEulerSwap; function eulerSwapPool() internal view virtual returns (address) { return address(0); @@ -158,4 +161,24 @@ abstract contract EulerSwapTest is AllowanceHolderPairTest { uint256 afterBalanceFrom = fromToken().balanceOf(FROM); assertEq(afterBalanceFrom + eulerSwapAmount(), beforeBalanceFrom); } + + function testSolvencyCheck() public skipIf(eulerSwapPool() == address(0)) setEulerSwapBlock { + IEulerSwap pool = IEulerSwap(eulerSwapPool()); + ParamsLib.Params params = pool.fastGetParams(); + + (uint256 reserve0, uint256 reserve1) = pool.fastGetReserves(); + uint256 amountOut = EulerSwapLib.findCurvePoint(eulerSwapAmount(), true, params, reserve0, reserve1); + assertTrue( + EulerSwapLib.checkSolvency( + EVC, + address(params.eulerAccount()), + address(params.vault0()), + address(params.vault1()), + true, + eulerSwapAmount(), + amountOut + ), + "Account is insolvent after swap" + ); + } } From d07cf360fbe2574142187d9bd9bfcbb2d179c6cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A1zaro=20Ra=C3=BAl=20Iglesias=20Vera?= Date: Tue, 22 Jul 2025 16:06:56 +0200 Subject: [PATCH 08/25] test: More test cases --- test/integration/EulerSwap.t.sol | 60 ++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/test/integration/EulerSwap.t.sol b/test/integration/EulerSwap.t.sol index a8d6c0731..9127144ab 100644 --- a/test/integration/EulerSwap.t.sol +++ b/test/integration/EulerSwap.t.sol @@ -181,4 +181,64 @@ abstract contract EulerSwapTest is AllowanceHolderPairTest { "Account is insolvent after swap" ); } + + function testSolvencyCheckReverse() public skipIf(eulerSwapPool() == address(0)) setEulerSwapBlock { + IEulerSwap pool = IEulerSwap(eulerSwapPool()); + ParamsLib.Params params = pool.fastGetParams(); + + (uint256 reserve0, uint256 reserve1) = pool.fastGetReserves(); + uint256 amountOut = EulerSwapLib.findCurvePoint(eulerSwapAmount(), false, params, reserve0, reserve1); + assertTrue( + EulerSwapLib.checkSolvency( + EVC, + address(params.eulerAccount()), + address(params.vault0()), + address(params.vault1()), + false, + eulerSwapAmount(), + amountOut + ), + "Account is insolvent after swap" + ); + } + + function testSolvencyCheckAtPoolLimit() public skipIf(eulerSwapPool() == address(0)) setEulerSwapBlock { + IEulerSwap pool = IEulerSwap(eulerSwapPool()); + ParamsLib.Params params = pool.fastGetParams(); + + (uint256 reserve0, uint256 reserve1) = pool.fastGetReserves(); + (uint256 amountIn, uint256 amountOut) = EulerSwapLib.calcLimits(EVC, pool, true, params, reserve0, reserve1); + assertTrue( + EulerSwapLib.checkSolvency( + EVC, + address(params.eulerAccount()), + address(params.vault0()), + address(params.vault1()), + true, + amountIn, + amountOut + ), + "Account is insolvent after swapping at pool limit" + ); + } + + function testSolvencyCheckAtPoolLimitReverse() public skipIf(eulerSwapPool() == address(0)) setEulerSwapBlock { + IEulerSwap pool = IEulerSwap(eulerSwapPool()); + ParamsLib.Params params = pool.fastGetParams(); + + (uint256 reserve0, uint256 reserve1) = pool.fastGetReserves(); + (uint256 amountIn, uint256 amountOut) = EulerSwapLib.calcLimits(EVC, pool, false, params, reserve0, reserve1); + assertTrue( + EulerSwapLib.checkSolvency( + EVC, + address(params.eulerAccount()), + address(params.vault0()), + address(params.vault1()), + false, + amountIn, + amountOut + ), + "Account is insolvent after swapping at pool limit" + ); + } } From 11212fb5fc6c86cb7d3a6d40b7d1743398e4f743 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A1zaro=20Ra=C3=BAl=20Iglesias=20Vera?= Date: Tue, 22 Jul 2025 16:32:20 +0200 Subject: [PATCH 09/25] fix: Ensure debt token is always used as it is without quoting it --- src/core/EulerSwap.sol | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/core/EulerSwap.sol b/src/core/EulerSwap.sol index 946be468e..7626e97e0 100644 --- a/src/core/EulerSwap.sol +++ b/src/core/EulerSwap.sol @@ -709,13 +709,21 @@ library EulerSwapLib { // collateral + newCollateral - soldCollateral >= debt // is changed to // collateral + newCollateral >= debt + soldCollateral - (uint256 value,) = oracle.fastGetQuote(soldCollateral, buyVault.fastAsset(), unitOfAccount); - debt += (value * debtVault.fastLTVBorrow(buyVault)); + if(buyVault == debtVault) { + debt += soldCollateral * 1e4; + } else { + (uint256 value,) = oracle.fastGetQuote(soldCollateral, buyVault.fastAsset(), unitOfAccount); + debt += (value * debtVault.fastLTVBorrow(buyVault)); + } } uint256 collateral; if(newCollateral != 0) { - (uint256 value,) = oracle.fastGetQuote(newCollateral, sellVault.fastAsset(), unitOfAccount); - collateral = (value * debtVault.fastLTVBorrow(sellVault)); + if(sellVault == debtVault) { + collateral = newCollateral * 1e4; + } else { + (uint256 value,) = oracle.fastGetQuote(newCollateral, sellVault.fastAsset(), unitOfAccount); + collateral = (value * debtVault.fastLTVBorrow(sellVault)); + } } for(uint256 i = 0; i < collaterals.length; i++) { IEVault collateralVault = IEVault(collaterals[i]); From 5f38eb69420e0a7fab40347fdfac58bc124b4904 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A1zaro=20Ra=C3=BAl=20Iglesias=20Vera?= Date: Tue, 22 Jul 2025 17:00:57 +0200 Subject: [PATCH 10/25] test: Insolvency test case --- test/integration/EulerSwap.t.sol | 53 +++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/test/integration/EulerSwap.t.sol b/test/integration/EulerSwap.t.sol index 9127144ab..aeb9a792f 100644 --- a/test/integration/EulerSwap.t.sol +++ b/test/integration/EulerSwap.t.sol @@ -11,7 +11,18 @@ import {Settler} from "src/Settler.sol"; import {SafeTransferLib} from "src/vendor/SafeTransferLib.sol"; -import {IEVC, IEulerSwap, EulerSwapLib, ParamsLib, FastEulerSwap} from "src/core/EulerSwap.sol"; +import { + IEVC, + IEulerSwap, + EulerSwapLib, + ParamsLib, + FastEulerSwap, + FastEvc, + IEVault, + FastEvault, + IOracle, + FastOracle +} from "src/core/EulerSwap.sol"; import {AllowanceHolderPairTest} from "./AllowanceHolderPairTest.t.sol"; @@ -19,9 +30,13 @@ IEVC constant EVC = IEVC(0x0C9a3dd6b8F28529d72d7f9cE918D493519EE383); abstract contract EulerSwapTest is AllowanceHolderPairTest { using SafeTransferLib for IERC20; + using SafeTransferLib for IEVault; using ParamsLib for IEulerSwap; using ParamsLib for ParamsLib.Params; using FastEulerSwap for IEulerSwap; + using FastEvc for IEVC; + using FastEvault for IEVault; + using FastOracle for IOracle; function eulerSwapPool() internal view virtual returns (address) { return address(0); @@ -241,4 +256,40 @@ abstract contract EulerSwapTest is AllowanceHolderPairTest { "Account is insolvent after swapping at pool limit" ); } + + function testSolvencyCheckFailsIfCollateralIsNotEnough() public skipIf(eulerSwapPool() == address(0)) setEulerSwapBlock { + IEulerSwap pool = IEulerSwap(eulerSwapPool()); + ParamsLib.Params params = pool.fastGetParams(); + address eulerAccount = address(params.eulerAccount()); + + address[] memory collaterals = EVC.fastGetCollaterals(eulerAccount); + address[] memory controllers = EVC.fastGetControllers(eulerAccount); + assertEq(controllers.length, 1, "Multiple debt vaults"); + assertEq(controllers[0], address(params.vault1()), "Debt vault is not vault1"); + + IEVault debtVault = IEVault(controllers[0]); + IOracle oracle = debtVault.fastOracle(); + IERC20 unitOfAccount = debtVault.fastUnitOfAccount(); + uint256 collateral; + for(uint256 i = 0; i < collaterals.length; i++) { + IEVault collateralVault = IEVault(collaterals[i]); + (uint256 value,) = oracle.fastGetQuote(collateralVault.fastBalanceOf(eulerAccount), collateralVault.fastAsset(), unitOfAccount); + + collateral += (value * debtVault.fastLTVBorrow(collateralVault)); + } + uint256 amountOut = (collateral - debtVault.fastDebtOf(eulerAccount) * 1e4) / 1e4; + + assertFalse( + EulerSwapLib.checkSolvency( + EVC, + address(params.eulerAccount()), + address(params.vault0()), + address(params.vault1()), + true, + 0, + amountOut + 1 + ), + "Account should be insolvent" + ); + } } From 26f6f9844524a485e609fd63490652dca09b3e87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A1zaro=20Ra=C3=BAl=20Iglesias=20Vera?= Date: Tue, 22 Jul 2025 17:54:46 +0200 Subject: [PATCH 11/25] chore: cleanup --- src/core/EulerSwap.sol | 22 +++++++--------------- test/integration/EulerSwap.t.sol | 6 ++++-- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/src/core/EulerSwap.sol b/src/core/EulerSwap.sol index 7626e97e0..0827eb40d 100644 --- a/src/core/EulerSwap.sol +++ b/src/core/EulerSwap.sol @@ -701,30 +701,22 @@ library EulerSwapLib { IOracle oracle = debtVault.fastOracle(); IERC20 unitOfAccount = debtVault.fastUnitOfAccount(); - // adjust precision to avoid divisions when adjusting LTVBorrow bps of collateral. - // All amounts in EulerSwap are lower than uint112 so there should not be overflow errors. - debt *= 1e4; + (, debt) = oracle.fastGetQuote(debt, debtVault.fastAsset(), unitOfAccount); + debt *= debtVault.fastLTVBorrow(debtVault); if(soldCollateral != 0) { // adjust inequality to avoid substraction // collateral + newCollateral - soldCollateral >= debt // is changed to // collateral + newCollateral >= debt + soldCollateral - if(buyVault == debtVault) { - debt += soldCollateral * 1e4; - } else { - (uint256 value,) = oracle.fastGetQuote(soldCollateral, buyVault.fastAsset(), unitOfAccount); - debt += (value * debtVault.fastLTVBorrow(buyVault)); - } + (uint256 value,) = oracle.fastGetQuote(soldCollateral, buyVault.fastAsset(), unitOfAccount); + debt += (value * debtVault.fastLTVBorrow(buyVault)); } uint256 collateral; if(newCollateral != 0) { - if(sellVault == debtVault) { - collateral = newCollateral * 1e4; - } else { - (uint256 value,) = oracle.fastGetQuote(newCollateral, sellVault.fastAsset(), unitOfAccount); - collateral = (value * debtVault.fastLTVBorrow(sellVault)); - } + (uint256 value,) = oracle.fastGetQuote(newCollateral, sellVault.fastAsset(), unitOfAccount); + collateral = (value * debtVault.fastLTVBorrow(sellVault)); } + for(uint256 i = 0; i < collaterals.length; i++) { IEVault collateralVault = IEVault(collaterals[i]); (uint256 value,) = oracle.fastGetQuote(collateralVault.fastBalanceOf(account), collateralVault.fastAsset(), unitOfAccount); diff --git a/test/integration/EulerSwap.t.sol b/test/integration/EulerSwap.t.sol index aeb9a792f..10458ed0f 100644 --- a/test/integration/EulerSwap.t.sol +++ b/test/integration/EulerSwap.t.sol @@ -277,7 +277,9 @@ abstract contract EulerSwapTest is AllowanceHolderPairTest { collateral += (value * debtVault.fastLTVBorrow(collateralVault)); } - uint256 amountOut = (collateral - debtVault.fastDebtOf(eulerAccount) * 1e4) / 1e4; + (, uint256 debt) = oracle.fastGetQuote(debtVault.fastDebtOf(eulerAccount), debtVault.fastAsset(), unitOfAccount); + debt *= debtVault.fastLTVBorrow(debtVault); + uint256 amountOut = (collateral - debt) / 1e4; assertFalse( EulerSwapLib.checkSolvency( @@ -287,7 +289,7 @@ abstract contract EulerSwapTest is AllowanceHolderPairTest { address(params.vault1()), true, 0, - amountOut + 1 + amountOut + 1e4 ), "Account should be insolvent" ); From d4a1d719fe2d6846a74b96a85bb239641278c2a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A1zaro=20Ra=C3=BAl=20Iglesias=20Vera?= Date: Tue, 22 Jul 2025 20:35:48 +0200 Subject: [PATCH 12/25] fix: review comments --- src/core/EulerSwap.sol | 154 +++++++++++++++++++------------ test/integration/EulerSwap.t.sol | 98 +++++++++++--------- 2 files changed, 147 insertions(+), 105 deletions(-) diff --git a/src/core/EulerSwap.sol b/src/core/EulerSwap.sol index 0827eb40d..828402f91 100644 --- a/src/core/EulerSwap.sol +++ b/src/core/EulerSwap.sol @@ -63,7 +63,7 @@ library FastEvc { function fastGetCollaterals(IEVC evc, address account) internal view returns (address[] memory collaterals) { assembly ("memory-safe") { mstore(0x14, account) - mstore(0x00, 0xa4d25d1e000000000000000000000000) // selector for `getCollaterals(address)` with `evc`'s padding + mstore(0x00, 0xa4d25d1e000000000000000000000000) // selector for `getCollaterals(address)` with `account`'s padding if iszero(staticcall(gas(), evc, 0x10, 0x24, 0x00, 0x00)) { let ptr := mload(0x40) @@ -81,7 +81,7 @@ library FastEvc { function fastGetControllers(IEVC evc, address account) internal view returns (address[] memory controllers) { assembly ("memory-safe") { mstore(0x14, account) - mstore(0x00, 0xfd6046d7000000000000000000000000) // selector for `getControllers(address)` with `evc`'s padding + mstore(0x00, 0xfd6046d7000000000000000000000000) // selector for `getControllers(address)` with `account`'s padding if iszero(staticcall(gas(), evc, 0x10, 0x24, 0x00, 0x00)) { let ptr := mload(0x40) @@ -116,21 +116,30 @@ interface IOracle { } library FastOracle { - function fastGetQuote(IOracle oracle, uint256 inAmount, IERC20 base, IERC20 quote) internal view returns (uint256 bidOutAmount, uint256 askOutAmount) { + function fastGetQuotes(IOracle oracle, uint256 inAmount, IERC20 base, IERC20 quote) + internal + view + returns (uint256 bidOutAmount, uint256 askOutAmount) + { assembly ("memory-safe") { let ptr := mload(0x40) - mstore(ptr, 0x0579e61f) // selector for `getQuotes(uint256,address,address)` - mstore(add(0x20, ptr), inAmount) - mstore(add(0x40, ptr), and(0xffffffffffffffffffffffffffffffffffffffff, base)) - mstore(add(0x60, ptr), and(0xffffffffffffffffffffffffffffffffffffffff, quote)) - if iszero(staticcall(gas(), oracle, add(0x1c, ptr), 0x64, 0x00, 0x40)) { + mstore(0x00, 0x0579e61f) // selector for `getQuotes(uint256,address,address)` + mstore(0x20, inAmount) + mstore(0x40, and(0xffffffffffffffffffffffffffffffffffffffff, base)) + mstore(0x60, and(0xffffffffffffffffffffffffffffffffffffffff, quote)) + if iszero(staticcall(gas(), oracle, 0x1c, 0x64, 0x00, 0x40)) { let ptr_ := mload(0x40) returndatacopy(ptr_, 0x00, returndatasize()) revert(ptr_, returndatasize()) } + if gt(0x40, returndatasize()) { revert(0x00, 0x00) } bidOutAmount := mload(0x00) askOutAmount := mload(0x20) + + // restore clobbered memory + mstore(0x40, ptr) + mstore(0x60, 0x00) } } } @@ -534,11 +543,14 @@ library EulerSwapLib { /// @param zeroForOne Boolean indicating whether asset0 (true) or asset1 (false) is the input token /// @return inLimit Maximum amount of input token that can be deposited /// @return outLimit Maximum amount of output token that can be withdrawn - function calcLimits(IEVC evc, IEulerSwap pool, bool zeroForOne, ParamsLib.Params p, uint256 reserve0, uint256 reserve1) - internal - view - returns (uint256 inLimit, uint256 outLimit) - { + function calcLimits( + IEVC evc, + IEulerSwap pool, + bool zeroForOne, + ParamsLib.Params p, + uint256 reserve0, + uint256 reserve1 + ) internal view returns (uint256 inLimit, uint256 outLimit) { IEVault sellVault; IEVault buyVault; { @@ -630,7 +642,7 @@ library EulerSwapLib { address vault0, address vault1, bool zeroForOne, - uint256 amountIn, + uint256 amountIn, uint256 amountOut ) internal view returns (bool) { address[] memory collaterals = evc.fastGetCollaterals(account); @@ -640,9 +652,11 @@ library EulerSwapLib { { address[] memory controllers = evc.fastGetControllers(account); - if(controllers.length > 1) return false; - if(controllers.length == 1) { - debtVault = IEVault(controllers[0]); + if (controllers.length > 1) return false; + if (controllers.length == 1) { + assembly ("memory-safe") { + debtVault := mload(add(0x20, controllers)) + } debt = debtVault.fastDebtOf(account); } } @@ -654,81 +668,101 @@ library EulerSwapLib { sellVault = IEVault(sellVault_); buyVault = IEVault(buyVault_); } - + uint256 newDebt; uint256 soldCollateral; uint256 newCollateral; // compute new debt in buyVault { - uint256 collateralBalance = buyVault.fastBalanceOf(account); - if(collateralBalance < amountOut) { - newDebt = amountOut - collateralBalance; + uint256 collateralBalance = buyVault.fastConvertToAssets(buyVault.fastBalanceOf(account)); + if (collateralBalance < amountOut) { + unchecked { + newDebt = amountOut - collateralBalance; + } soldCollateral = collateralBalance; - } - else { + } else { soldCollateral = amountOut; } } // check sellVault debt - if(debtVault == sellVault) { + if (debtVault == sellVault) { // debt repayment - if(amountIn < debt) { + if (amountIn < debt) { // collateral in buyVault must be able to cover the amountOut // to ensure there is only one controller at the end - if(newDebt != 0) return false; - debt -= amountIn; - } - else { - newCollateral = amountIn - debt; + if (newDebt != 0) return false; + unchecked { + debt -= amountIn; + } + } else { + unchecked { + newCollateral = amountIn - debt; + } debt = 0; // debt was repaid } } else { newCollateral = amountIn; } - // if there is new debt in buyVault, check that the controller is buyVault - // or debt was repaid to ensure there is only one controller at the end - if(newDebt != 0) { - if(debt != 0 && debtVault != buyVault) return false; - if(address(debtVault) == address(0)) debtVault = buyVault; + if (newDebt != 0) { + // There is new debt in buyVault. Then debtVault needs to be buyVault. + if (debtVault != buyVault) { + // If it is not and there is outstanding debt, then there is a second controller + // which is not allowed. + if (debt != 0) return false; + debtVault = buyVault; + } debt += newDebt; } - + // check for solvency if there is any debt - if(debt != 0) { + if (debt != 0) { IOracle oracle = debtVault.fastOracle(); IERC20 unitOfAccount = debtVault.fastUnitOfAccount(); - (, debt) = oracle.fastGetQuote(debt, debtVault.fastAsset(), unitOfAccount); - debt *= debtVault.fastLTVBorrow(debtVault); - if(soldCollateral != 0) { - // adjust inequality to avoid substraction - // collateral + newCollateral - soldCollateral >= debt - // is changed to - // collateral + newCollateral >= debt + soldCollateral - (uint256 value,) = oracle.fastGetQuote(soldCollateral, buyVault.fastAsset(), unitOfAccount); - debt += (value * debtVault.fastLTVBorrow(buyVault)); - } + (, debt) = oracle.fastGetQuotes(debt, debtVault.fastAsset(), unitOfAccount); + // multiply by 1e4 to avoid divisions when adjusting LTVBorrow bps of collaterals. + // All amounts in EulerSwap are lower than uint112 so there should not be overflow errors. + debt *= 1e4; uint256 collateral; - if(newCollateral != 0) { - (uint256 value,) = oracle.fastGetQuote(newCollateral, sellVault.fastAsset(), unitOfAccount); - collateral = (value * debtVault.fastLTVBorrow(sellVault)); - } - - for(uint256 i = 0; i < collaterals.length; i++) { - IEVault collateralVault = IEVault(collaterals[i]); - (uint256 value,) = oracle.fastGetQuote(collateralVault.fastBalanceOf(account), collateralVault.fastAsset(), unitOfAccount); - - collateral += (value * debtVault.fastLTVBorrow(collateralVault)); - if(collateral >= debt) { - return true; + for (uint256 i = 1; i <= collaterals.length; i++) { + IEVault collateralVault; + assembly ("memory-safe") { + collateralVault := mload(add(mul(0x20, i), collaterals)) + } + uint256 collateralAmount = collateralVault.fastConvertToAssets(collateralVault.fastBalanceOf(account)); + if (collateralVault == sellVault) { + collateralAmount += newCollateral; + newCollateral = 0; + } else if (collateralVault == buyVault) { + unchecked { + collateralAmount -= soldCollateral; + } } + if (collateralAmount != 0) { + (uint256 value,) = oracle.fastGetQuotes( + collateralAmount, + collateralVault.fastAsset(), + unitOfAccount + ); + + collateral += (value * debtVault.fastLTVBorrow(collateralVault)); + if (collateral >= debt) { + return true; + } + } + } + if (newCollateral != 0) { + // Sell vault was not in the collaterals + (uint256 value,) = oracle.fastGetQuotes(newCollateral, sellVault.fastAsset(), unitOfAccount); + collateral += (value * debtVault.fastLTVBorrow(sellVault)); } + return (collateral >= debt); } - return false; + return true; } } diff --git a/test/integration/EulerSwap.t.sol b/test/integration/EulerSwap.t.sol index 10458ed0f..68809ff45 100644 --- a/test/integration/EulerSwap.t.sol +++ b/test/integration/EulerSwap.t.sol @@ -12,15 +12,15 @@ import {Settler} from "src/Settler.sol"; import {SafeTransferLib} from "src/vendor/SafeTransferLib.sol"; import { - IEVC, - IEulerSwap, - EulerSwapLib, - ParamsLib, - FastEulerSwap, - FastEvc, - IEVault, + IEVC, + IEulerSwap, + EulerSwapLib, + ParamsLib, + FastEulerSwap, + FastEvc, + IEVault, FastEvault, - IOracle, + IOracle, FastOracle } from "src/core/EulerSwap.sol"; @@ -185,12 +185,12 @@ abstract contract EulerSwapTest is AllowanceHolderPairTest { uint256 amountOut = EulerSwapLib.findCurvePoint(eulerSwapAmount(), true, params, reserve0, reserve1); assertTrue( EulerSwapLib.checkSolvency( - EVC, - address(params.eulerAccount()), - address(params.vault0()), - address(params.vault1()), - true, - eulerSwapAmount(), + EVC, + address(params.eulerAccount()), + address(params.vault0()), + address(params.vault1()), + true, + eulerSwapAmount(), amountOut ), "Account is insolvent after swap" @@ -205,12 +205,12 @@ abstract contract EulerSwapTest is AllowanceHolderPairTest { uint256 amountOut = EulerSwapLib.findCurvePoint(eulerSwapAmount(), false, params, reserve0, reserve1); assertTrue( EulerSwapLib.checkSolvency( - EVC, - address(params.eulerAccount()), - address(params.vault0()), - address(params.vault1()), - false, - eulerSwapAmount(), + EVC, + address(params.eulerAccount()), + address(params.vault0()), + address(params.vault1()), + false, + eulerSwapAmount(), amountOut ), "Account is insolvent after swap" @@ -225,12 +225,12 @@ abstract contract EulerSwapTest is AllowanceHolderPairTest { (uint256 amountIn, uint256 amountOut) = EulerSwapLib.calcLimits(EVC, pool, true, params, reserve0, reserve1); assertTrue( EulerSwapLib.checkSolvency( - EVC, - address(params.eulerAccount()), - address(params.vault0()), - address(params.vault1()), - true, - amountIn, + EVC, + address(params.eulerAccount()), + address(params.vault0()), + address(params.vault1()), + true, + amountIn, amountOut ), "Account is insolvent after swapping at pool limit" @@ -245,19 +245,23 @@ abstract contract EulerSwapTest is AllowanceHolderPairTest { (uint256 amountIn, uint256 amountOut) = EulerSwapLib.calcLimits(EVC, pool, false, params, reserve0, reserve1); assertTrue( EulerSwapLib.checkSolvency( - EVC, - address(params.eulerAccount()), - address(params.vault0()), - address(params.vault1()), - false, - amountIn, + EVC, + address(params.eulerAccount()), + address(params.vault0()), + address(params.vault1()), + false, + amountIn, amountOut ), "Account is insolvent after swapping at pool limit" ); } - function testSolvencyCheckFailsIfCollateralIsNotEnough() public skipIf(eulerSwapPool() == address(0)) setEulerSwapBlock { + function testSolvencyCheckFailsIfCollateralIsNotEnough() + public + skipIf(eulerSwapPool() == address(0)) + setEulerSwapBlock + { IEulerSwap pool = IEulerSwap(eulerSwapPool()); ParamsLib.Params params = pool.fastGetParams(); address eulerAccount = address(params.eulerAccount()); @@ -271,25 +275,29 @@ abstract contract EulerSwapTest is AllowanceHolderPairTest { IOracle oracle = debtVault.fastOracle(); IERC20 unitOfAccount = debtVault.fastUnitOfAccount(); uint256 collateral; - for(uint256 i = 0; i < collaterals.length; i++) { + for (uint256 i = 0; i < collaterals.length; i++) { IEVault collateralVault = IEVault(collaterals[i]); - (uint256 value,) = oracle.fastGetQuote(collateralVault.fastBalanceOf(eulerAccount), collateralVault.fastAsset(), unitOfAccount); + (uint256 value,) = oracle.fastGetQuotes( + collateralVault.fastConvertToAssets(collateralVault.fastBalanceOf(eulerAccount)), + collateralVault.fastAsset(), + unitOfAccount + ); collateral += (value * debtVault.fastLTVBorrow(collateralVault)); } - (, uint256 debt) = oracle.fastGetQuote(debtVault.fastDebtOf(eulerAccount), debtVault.fastAsset(), unitOfAccount); - debt *= debtVault.fastLTVBorrow(debtVault); - uint256 amountOut = (collateral - debt) / 1e4; + (, uint256 debt) = + oracle.fastGetQuotes(debtVault.fastDebtOf(eulerAccount), debtVault.fastAsset(), unitOfAccount); + uint256 amountOut = (collateral - debt * 1e4) / 1e4; assertFalse( EulerSwapLib.checkSolvency( - EVC, - address(params.eulerAccount()), - address(params.vault0()), - address(params.vault1()), - true, - 0, - amountOut + 1e4 + EVC, + address(params.eulerAccount()), + address(params.vault0()), + address(params.vault1()), + true, + 0, + amountOut + 1 ), "Account should be insolvent" ); From bc4d18c7b320682460fd85314a997d5436efaef9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A1zaro=20Ra=C3=BAl=20Iglesias=20Vera?= Date: Tue, 22 Jul 2025 20:37:25 +0200 Subject: [PATCH 13/25] chore: fmt --- src/core/EulerSwap.sol | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/core/EulerSwap.sol b/src/core/EulerSwap.sol index 828402f91..84ea6021b 100644 --- a/src/core/EulerSwap.sol +++ b/src/core/EulerSwap.sol @@ -742,12 +742,8 @@ library EulerSwapLib { } } if (collateralAmount != 0) { - (uint256 value,) = oracle.fastGetQuotes( - collateralAmount, - collateralVault.fastAsset(), - unitOfAccount - ); - + (uint256 value,) = + oracle.fastGetQuotes(collateralAmount, collateralVault.fastAsset(), unitOfAccount); collateral += (value * debtVault.fastLTVBorrow(collateralVault)); if (collateral >= debt) { return true; From a6514296e91698ad27543a49bede58c355da9160 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A1zaro=20Ra=C3=BAl=20Iglesias=20Vera?= Date: Tue, 22 Jul 2025 23:42:43 +0200 Subject: [PATCH 14/25] doc: Multi controllers scenarios details --- src/core/EulerSwap.sol | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/core/EulerSwap.sol b/src/core/EulerSwap.sol index 84ea6021b..b207198ea 100644 --- a/src/core/EulerSwap.sol +++ b/src/core/EulerSwap.sol @@ -691,8 +691,18 @@ library EulerSwapLib { // debt repayment if (amountIn < debt) { // collateral in buyVault must be able to cover the amountOut - // to ensure there is only one controller at the end - if (newDebt != 0) return false; + // to ensure there is only one controller at the end. + if (newDebt != 0) { + // One way this can be possible is: + // 1. TokenA and TokenB are the tokens in the pool with respectively VaultA and VaultB + // 2. Pool is collateralized by TokenC (which might be one of the tokens in the pool) + // 3. After some trading there is debt in TokenA and credit in TokenB, which means that + // VaultA is controller and VaultB is an enabled collateral. + // 4. The pool owner withdraws some of the credit in TokenB + // 5. Then if the pools gets back to equilibrium there is going to be debt in both + // TokenA and TokenB, meaning that there are going to be two controllers at the end. + return false; + } unchecked { debt -= amountIn; } @@ -711,7 +721,17 @@ library EulerSwapLib { if (debtVault != buyVault) { // If it is not and there is outstanding debt, then there is a second controller // which is not allowed. - if (debt != 0) return false; + if (debt != 0) { + // One way this can be possible is: + // 1. TokenA and TokenB are the tokens in the pool with respectively VaultA and VaultB + // 2. Pool is collateralized with both TokenA and TokenB + // 3. The pool owner borrows some TokenC which generates debt and makes VaultC + // the controller in the pool + // 4. After some trading collateral in TokenA is fully withdrawn + // 5. Any subsequent trade to buy TokenA will generate debt and make + // VaultA a second controller. + return false; + } debtVault = buyVault; } debt += newDebt; From 3211cb5e640d88b973c7a579f4ededa7b6449bd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A1zaro=20Ra=C3=BAl=20Iglesias=20Vera?= Date: Tue, 22 Jul 2025 23:53:26 +0200 Subject: [PATCH 15/25] doc: solve TODO --- src/core/EulerSwap.sol | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/core/EulerSwap.sol b/src/core/EulerSwap.sol index b207198ea..f24187770 100644 --- a/src/core/EulerSwap.sol +++ b/src/core/EulerSwap.sol @@ -826,9 +826,12 @@ abstract contract EulerSwap is SettlerAbstract { unchecked { sellAmount = sellToken.fastBalanceOf(address(this)) * bps / BASIS; } - // TODO: Comment for what happens with excess here. + // If the sell amount is over the limit: // 1. Excess is absorbed by other sources - // 2. Ultimatelly donated as fee, which might result in a slippage revert? Double check. + // 2. Donated as fee, which might result in a slippage revert + // 3. Left in settler if there is no slippage collection action + // or it is not absorbed. This might result in slippage revert and, + // if not, assets will be compromissed and tentatively taken away. sellAmount = (sellAmount > inLimit).ternary(inLimit, sellAmount); sellToken.safeTransfer(address(pool), sellAmount); } From 417a2ec401d229152ad483fe930ac17a67a14669 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Thu, 24 Jul 2025 11:52:18 -0400 Subject: [PATCH 16/25] Cleanup --- src/core/EulerSwap.sol | 166 ++++++++++++++++++++++++++++------------- 1 file changed, 113 insertions(+), 53 deletions(-) diff --git a/src/core/EulerSwap.sol b/src/core/EulerSwap.sol index f24187770..c8e84fa9b 100644 --- a/src/core/EulerSwap.sol +++ b/src/core/EulerSwap.sol @@ -27,7 +27,7 @@ interface IEVC { /// controller vault. /// @param account The address of the account whose collaterals are being queried. /// @return An array of addresses that are enabled collaterals for the account. - function getCollaterals(address account) external view returns (address[] memory); + function getCollaterals(address account) external view returns (IEVault[] memory); /// @notice Returns an array of enabled controllers for an account. /// @dev A controller is a vault that has been chosen for an account to have special control over the account's @@ -35,7 +35,7 @@ interface IEVC { /// most one can be selected when the account status check is performed. /// @param account The address of the account whose controllers are being queried. /// @return An array of addresses that are the enabled controllers for the account. - function getControllers(address account) external view returns (address[] memory); + function getControllers(address account) external view returns (IEVault[] memory); } library FastEvc { @@ -60,7 +60,7 @@ library FastEvc { } } - function fastGetCollaterals(IEVC evc, address account) internal view returns (address[] memory collaterals) { + function fastGetCollaterals(IEVC evc, address account) internal view returns (IEVault[] memory collaterals) { assembly ("memory-safe") { mstore(0x14, account) mstore(0x00, 0xa4d25d1e000000000000000000000000) // selector for `getCollaterals(address)` with `account`'s padding @@ -78,7 +78,7 @@ library FastEvc { } } - function fastGetControllers(IEVC evc, address account) internal view returns (address[] memory controllers) { + function fastGetControllers(IEVC evc, address account) internal view returns (IEVault[] memory controllers) { assembly ("memory-safe") { mstore(0x14, account) mstore(0x00, 0xfd6046d7000000000000000000000000) // selector for `getControllers(address)` with `account`'s padding @@ -487,6 +487,45 @@ library ParamsLib { } } +type EVaultIterator is uint256; + +library LibEVaultArray { + function iter(IEVault[] memory a) internal pure returns (EVaultIterator i) { + assembly ("memory-safe") { + i := add(0x20, a) + } + } + + function end(IEVault[] memory a) internal pure returns (EVaultIterator i) { + assembly ("memory-safe") { + i := add(0x20, add(shl(0x05, mload(a)), a)) + } + } + + function next(EVaultIterator i) internal pure returns (EVaultIterator) { + unchecked { + return EVaultIterator.wrap(32 + EVaultIterator.unwrap(i)); + } + } + + function get(IEVault[] memory, EVaultIterator i) internal pure returns (IEVault r) { + assembly ("memory-safe") { + r := mload(i) + } + } +} + +function __EVaultIterator_eq(EVaultIterator a, EVaultIterator b) pure returns (bool) { + return EVaultIterator.unwrap(a) == EVaultIterator.unwrap(b); +} + +function __EVaultIterator_ne(EVaultIterator a, EVaultIterator b) pure returns (bool) { + return EVaultIterator.unwrap(a) != EVaultIterator.unwrap(b); +} + +using {__EVaultIterator_eq as ==, __EVaultIterator_ne as !=} for EVaultIterator global; + + library EulerSwapLib { using UnsafeMath for uint256; using Math for uint256; @@ -496,6 +535,8 @@ library EulerSwapLib { using FastEvc for IEVC; using FastEvault for IEVault; using FastOracle for IOracle; + using LibEVaultArray for IEVault[]; + using LibEVaultArray for EVaultIterator; function findCurvePoint(uint256 amount, bool zeroForOne, ParamsLib.Params p, uint256 reserve0, uint256 reserve1) internal @@ -645,18 +686,21 @@ library EulerSwapLib { uint256 amountIn, uint256 amountOut ) internal view returns (bool) { - address[] memory collaterals = evc.fastGetCollaterals(account); + IEVault[] memory collaterals = evc.fastGetCollaterals(account); + // The EVC enforces that there can be at most 1 controller for an Euler + // account. Consequently, there is only 1 vault in which the account can incur debt. If + // there is no controller (i.e. no debt) then `debtVault` will be zero. IEVault debtVault; + // `debt` is the outstanding debt owed by the Euler account to `debtVault`. If + // `debtVault.asset()` is the sell token and `amountIn > debt`, then `debt` will be zero. uint256 debt; { - address[] memory controllers = evc.fastGetControllers(account); + IEVault[] memory controllers = evc.fastGetControllers(account); if (controllers.length > 1) return false; if (controllers.length == 1) { - assembly ("memory-safe") { - debtVault := mload(add(0x20, controllers)) - } + debtVault = controllers.get(controllers.iter()); debt = debtVault.fastDebtOf(account); } } @@ -669,11 +713,20 @@ library EulerSwapLib { buyVault = IEVault(buyVault_); } + // `newDebt` is new, underlying-denominated debt in the buy token incurred after the + // swap. It is zero if the swap only results in repaying debt (increasing the health + // factor). uint256 newDebt; + // `soldCollateral` is the new, underlying-denominated amount of collateral in the buy token + // that is removed from the account and given to the user. If the buy amount exceeds the + // amount of buy-token collateral available, then the value is `amountOut`. uint256 soldCollateral; + // `newCollateral` is the new, underlying-denominated amount of sell token collateral in the + // account after the swap. If `amountIn` is less than the current sell token debt, then + // `newCollateral` is zero. uint256 newCollateral; - // compute new debt in buyVault + // Compute the effect of sending `amountOut` of the buy token to the taker. { uint256 collateralBalance = buyVault.fastConvertToAssets(buyVault.fastBalanceOf(account)); if (collateralBalance < amountOut) { @@ -686,50 +739,61 @@ library EulerSwapLib { } } - // check sellVault debt + // Compute the effect of receiving `amountIn` of the sell token from the taker. if (debtVault == sellVault) { - // debt repayment + // We are repaying debt; we have to check whether this will cause us to disable + // `sellVault` as the controller. if (amountIn < debt) { - // collateral in buyVault must be able to cover the amountOut - // to ensure there is only one controller at the end. + // We are doing a partial repayment of the debt. We have to check for the edge case + // where we could end up with 2 controllers (forbidden by the EVC). if (newDebt != 0) { - // One way this can be possible is: - // 1. TokenA and TokenB are the tokens in the pool with respectively VaultA and VaultB - // 2. Pool is collateralized by TokenC (which might be one of the tokens in the pool) - // 3. After some trading there is debt in TokenA and credit in TokenB, which means that - // VaultA is controller and VaultB is an enabled collateral. - // 4. The pool owner withdraws some of the credit in TokenB - // 5. Then if the pools gets back to equilibrium there is going to be debt in both - // TokenA and TokenB, meaning that there are going to be two controllers at the end. + // We would end up with 2 controllers. Here's a hypothetical scenario: assume + // that `tokenA` and `tokenB` are the underlying tokens in the pool with vault + // `vaultA` and `vaultB`, respectively. Further assume that the Euler account is + // collateralized by `tokenC` (which might be one of `tokenA` or `tokenB`, but + // it doesn't matter). After some trading, there is debt in `tokenA` and credit + // in `tokenB` (i.e. `tokenA` is the buy token and `tokenB` is the sell token), + // which means that `vaultA` is the controller and `vaultB` is an enabled + // collateral. The owner of the Euler account where the pool is an operator + // withdraws some of the credit in `tokenB`. When the pool returns to + // equilibrium (i.e. `reserve0 == equilibriumReserve0 && reserve1 == + // equilibriumReserve1`), there will be debt in both `tokenA` and `tokenB`. This + // would require both `vaultA` and `vaultB` to be controllers, which is + // forbidden. return false; } - unchecked { - debt -= amountIn; - } } else { unchecked { newCollateral = amountIn - debt; } - debt = 0; // debt was repaid } + debt = debt.saturatingSub(amountIn); } else { newCollateral = amountIn; } if (newDebt != 0) { - // There is new debt in buyVault. Then debtVault needs to be buyVault. + // If we have incurred debt in `buyVault`, then `buyVault` must already be + // `debtVault`. If this were not the case, then the EVC would revert because we were + // trying to release the lock while there are 2 controllers. if (debtVault != buyVault) { + // It is allowed to incur debt in a different vault iff all outstanding debt would + // be repaid. The pool will automatically disable the controller of the Euler + // account when the debt is repaid. + // If it is not and there is outstanding debt, then there is a second controller // which is not allowed. if (debt != 0) { - // One way this can be possible is: - // 1. TokenA and TokenB are the tokens in the pool with respectively VaultA and VaultB - // 2. Pool is collateralized with both TokenA and TokenB - // 3. The pool owner borrows some TokenC which generates debt and makes VaultC - // the controller in the pool - // 4. After some trading collateral in TokenA is fully withdrawn - // 5. Any subsequent trade to buy TokenA will generate debt and make - // VaultA a second controller. + // The outstanding debt was not entirely repaid. This would create 2 + // controllers, which the EVC enforces as invalid. Here's a hypothetical + // scenario: assume that `tokenA` and `tokenB` are the underlying tokens in the + // pool with vault `vaultA` and `vaultB`, respectively. Assume that the Euler + // account has no debt in either `vaultA` or `vaultB`. The owner of the Euler + // account borrows `tokenC` from `vaultC` that is neither `vaultA` nor + // `vaultB`. This creates debt and enables `vaultC` as the controller of the + // account. After some trading against the pool, the Euler account incurs a debt + // in `tokenA` turning `vaultA` on as a controller. This is invalid because both + // `vaultA` and `vaultC` would be controllers of the account. return false; } debtVault = buyVault; @@ -737,21 +801,20 @@ library EulerSwapLib { debt += newDebt; } - // check for solvency if there is any debt + // We now know the post-swap state of the pool. Adjust collateral for LTV and convert both + // collateral and debt into the unit of account for solvency. if (debt != 0) { IOracle oracle = debtVault.fastOracle(); IERC20 unitOfAccount = debtVault.fastUnitOfAccount(); (, debt) = oracle.fastGetQuotes(debt, debtVault.fastAsset(), unitOfAccount); - // multiply by 1e4 to avoid divisions when adjusting LTVBorrow bps of collaterals. - // All amounts in EulerSwap are lower than uint112 so there should not be overflow errors. + // Debt is not LTV adjusted. LTV is in basis points. By multiplying the debt bt 10_000, + // we can avoid rounding error in the solvency calculation. Overflow is not possible + // because debt must be representable as a `uint112`. debt *= 1e4; - uint256 collateral; - for (uint256 i = 1; i <= collaterals.length; i++) { - IEVault collateralVault; - assembly ("memory-safe") { - collateralVault := mload(add(mul(0x20, i), collaterals)) - } + uint256 collateral; // the sum of all LTV-adjusted, unit-of-account valued collaterals + for ((EVaultIterator i, EVaultIterator end) = (collaterals.iter(), collaterals.end()); i != end; i = i.next()) { + IEVault collateralVault = collaterals.get(i); uint256 collateralAmount = collateralVault.fastConvertToAssets(collateralVault.fastBalanceOf(account)); if (collateralVault == sellVault) { collateralAmount += newCollateral; @@ -775,10 +838,10 @@ library EulerSwapLib { (uint256 value,) = oracle.fastGetQuotes(newCollateral, sellVault.fastAsset(), unitOfAccount); collateral += (value * debtVault.fastLTVBorrow(sellVault)); } - return (collateral >= debt); + return collateral >= debt; + } else { + return true; } - - return true; } } @@ -826,12 +889,9 @@ abstract contract EulerSwap is SettlerAbstract { unchecked { sellAmount = sellToken.fastBalanceOf(address(this)) * bps / BASIS; } - // If the sell amount is over the limit: - // 1. Excess is absorbed by other sources - // 2. Donated as fee, which might result in a slippage revert - // 3. Left in settler if there is no slippage collection action - // or it is not absorbed. This might result in slippage revert and, - // if not, assets will be compromissed and tentatively taken away. + // If the sell amount is over the limit, any excess will be retained by Settler and sold + // to subsequent liquidities in the actions list. If `pool` is the last liquidity, this + // will almost certainly result in a slippage revert. sellAmount = (sellAmount > inLimit).ternary(inLimit, sellAmount); sellToken.safeTransfer(address(pool), sellAmount); } From d74f475a4acac414a2cd47011cf3f0fd781b8c17 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Thu, 24 Jul 2025 11:54:53 -0400 Subject: [PATCH 17/25] `forge fmt` --- src/core/EulerSwap.sol | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/core/EulerSwap.sol b/src/core/EulerSwap.sol index c8e84fa9b..e1a06fcc7 100644 --- a/src/core/EulerSwap.sol +++ b/src/core/EulerSwap.sol @@ -525,7 +525,6 @@ function __EVaultIterator_ne(EVaultIterator a, EVaultIterator b) pure returns (b using {__EVaultIterator_eq as ==, __EVaultIterator_ne as !=} for EVaultIterator global; - library EulerSwapLib { using UnsafeMath for uint256; using Math for uint256; @@ -813,7 +812,9 @@ library EulerSwapLib { // because debt must be representable as a `uint112`. debt *= 1e4; uint256 collateral; // the sum of all LTV-adjusted, unit-of-account valued collaterals - for ((EVaultIterator i, EVaultIterator end) = (collaterals.iter(), collaterals.end()); i != end; i = i.next()) { + for ( + (EVaultIterator i, EVaultIterator end) = (collaterals.iter(), collaterals.end()); i != end; i = i.next() + ) { IEVault collateralVault = collaterals.get(i); uint256 collateralAmount = collateralVault.fastConvertToAssets(collateralVault.fastBalanceOf(account)); if (collateralVault == sellVault) { From 8707f95dbbf9525655a590e06a222cbce2b9d5b2 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Fri, 25 Jul 2025 18:39:29 -0400 Subject: [PATCH 18/25] Cleanup --- src/core/EulerSwap.sol | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/core/EulerSwap.sol b/src/core/EulerSwap.sol index e1a06fcc7..d22ebfd7d 100644 --- a/src/core/EulerSwap.sol +++ b/src/core/EulerSwap.sol @@ -697,7 +697,11 @@ library EulerSwapLib { { IEVault[] memory controllers = evc.fastGetControllers(account); - if (controllers.length > 1) return false; + if (controllers.length > 1) { + // Not possible unless we're already inside a EVC batch with deferred checks. An + // account that already has its checks deferred cannot be swapped against. + return false; + } if (controllers.length == 1) { debtVault = controllers.get(controllers.iter()); debt = debtVault.fastDebtOf(account); @@ -797,7 +801,9 @@ library EulerSwapLib { } debtVault = buyVault; } - debt += newDebt; + unchecked { + debt += newDebt; + } } // We now know the post-swap state of the pool. Adjust collateral for LTV and convert both @@ -807,10 +813,12 @@ library EulerSwapLib { IERC20 unitOfAccount = debtVault.fastUnitOfAccount(); (, debt) = oracle.fastGetQuotes(debt, debtVault.fastAsset(), unitOfAccount); - // Debt is not LTV adjusted. LTV is in basis points. By multiplying the debt bt 10_000, + // Debt is not LTV adjusted. LTV is in basis points. By multiplying the debt by 10_000, // we can avoid rounding error in the solvency calculation. Overflow is not possible // because debt must be representable as a `uint112`. - debt *= 1e4; + unchecked { + debt *= 1e4; + } uint256 collateral; // the sum of all LTV-adjusted, unit-of-account valued collaterals for ( (EVaultIterator i, EVaultIterator end) = (collaterals.iter(), collaterals.end()); i != end; i = i.next() From d09a8212220350b795c857f9bb0399679b09f738 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Fri, 25 Jul 2025 18:39:49 -0400 Subject: [PATCH 19/25] Cleanup --- src/core/EulerSwap.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/core/EulerSwap.sol b/src/core/EulerSwap.sol index d22ebfd7d..2fb4036b3 100644 --- a/src/core/EulerSwap.sol +++ b/src/core/EulerSwap.sol @@ -836,7 +836,9 @@ library EulerSwapLib { if (collateralAmount != 0) { (uint256 value,) = oracle.fastGetQuotes(collateralAmount, collateralVault.fastAsset(), unitOfAccount); - collateral += (value * debtVault.fastLTVBorrow(collateralVault)); + unchecked { + collateral += (value * debtVault.fastLTVBorrow(collateralVault)); + } if (collateral >= debt) { return true; } From 13e448e2d96a63e0b15dc229a91cb286e551a92e Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Fri, 25 Jul 2025 18:43:00 -0400 Subject: [PATCH 20/25] Cleanup --- src/core/EulerSwap.sol | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/core/EulerSwap.sol b/src/core/EulerSwap.sol index 2fb4036b3..a5bad7bff 100644 --- a/src/core/EulerSwap.sol +++ b/src/core/EulerSwap.sol @@ -826,7 +826,9 @@ library EulerSwapLib { IEVault collateralVault = collaterals.get(i); uint256 collateralAmount = collateralVault.fastConvertToAssets(collateralVault.fastBalanceOf(account)); if (collateralVault == sellVault) { - collateralAmount += newCollateral; + unchecked { + collateralAmount += newCollateral; + } newCollateral = 0; } else if (collateralVault == buyVault) { unchecked { @@ -845,9 +847,12 @@ library EulerSwapLib { } } if (newCollateral != 0) { - // Sell vault was not in the collaterals + // Sell vault was not in the collaterals. The pool enables the collateral for the + // account before releasing the EVC. (uint256 value,) = oracle.fastGetQuotes(newCollateral, sellVault.fastAsset(), unitOfAccount); - collateral += (value * debtVault.fastLTVBorrow(sellVault)); + unchecked { + collateral += (value * debtVault.fastLTVBorrow(sellVault)); + } } return collateral >= debt; } else { From 9b35bc24228add831c568545d1cd942a2d64e3a8 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Fri, 25 Jul 2025 18:45:44 -0400 Subject: [PATCH 21/25] Compilation errors --- test/integration/EulerSwap.t.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/integration/EulerSwap.t.sol b/test/integration/EulerSwap.t.sol index 68809ff45..cfe836073 100644 --- a/test/integration/EulerSwap.t.sol +++ b/test/integration/EulerSwap.t.sol @@ -266,10 +266,10 @@ abstract contract EulerSwapTest is AllowanceHolderPairTest { ParamsLib.Params params = pool.fastGetParams(); address eulerAccount = address(params.eulerAccount()); - address[] memory collaterals = EVC.fastGetCollaterals(eulerAccount); - address[] memory controllers = EVC.fastGetControllers(eulerAccount); + IEVault[] memory collaterals = EVC.fastGetCollaterals(eulerAccount); + IEVault[] memory controllers = EVC.fastGetControllers(eulerAccount); assertEq(controllers.length, 1, "Multiple debt vaults"); - assertEq(controllers[0], address(params.vault1()), "Debt vault is not vault1"); + assertEq(address(controllers[0]), address(params.vault1()), "Debt vault is not vault1"); IEVault debtVault = IEVault(controllers[0]); IOracle oracle = debtVault.fastOracle(); From 1d2dd1110e84c719f3add542612472b7ed525725 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Fri, 25 Jul 2025 18:47:55 -0400 Subject: [PATCH 22/25] `CHANGELOG.md` --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38474be5c..e98ec86d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,8 @@ Master list of UniV3 forks: ### Breaking changes * Update Scroll to the Cancun hardfork +* Improve gas efficiency and accuracy of `EULERSWAP` action +* Add solvency check for EulerSwap (does not execute on-chain) ### Non-breaking changes From 9738389691fa18f01317a9c4abcd8a80e2671e86 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Sat, 26 Jul 2025 13:23:28 -0400 Subject: [PATCH 23/25] Stack too deep --- src/core/EulerSwap.sol | 58 ++++++++++++++++++------------------------ 1 file changed, 25 insertions(+), 33 deletions(-) diff --git a/src/core/EulerSwap.sol b/src/core/EulerSwap.sol index a5bad7bff..eddda897e 100644 --- a/src/core/EulerSwap.sol +++ b/src/core/EulerSwap.sol @@ -542,31 +542,28 @@ library EulerSwapLib { pure returns (uint256) { - uint256 px = p.priceX(); - uint256 py = p.priceY(); - uint256 x0 = p.equilibriumReserve0(); - uint256 y0 = p.equilibriumReserve1(); - unchecked { uint256 amountWithFee = amount - (amount * p.fee() / 1e18); if (zeroForOne) { // swap X in and Y out uint256 xNew = reserve0 + amountWithFee; + uint256 x0 = p.equilibriumReserve0(); uint256 yNew = xNew <= x0 // remain on f() - ? CurveLib.saturatingF(xNew, px, py, x0, y0, p.concentrationX()) + ? CurveLib.saturatingF(xNew, p.priceX(), p.priceY(), x0, p.equilibriumReserve1(), p.concentrationX()) // move to g() - : CurveLib.fInverse(xNew, py, px, y0, x0, p.concentrationY()); + : CurveLib.fInverse(xNew, p.priceY(), p.priceX(), p.equilibriumReserve1(), x0, p.concentrationY()); yNew = yNew.unsafeInc(yNew == 0); return reserve1.saturatingSub(yNew); } else { // swap Y in and X out uint256 yNew = reserve1 + amountWithFee; + uint256 y0 = p.equilibriumReserve1(); uint256 xNew = yNew <= y0 // remain on g() - ? CurveLib.saturatingF(yNew, py, px, y0, x0, p.concentrationY()) + ? CurveLib.saturatingF(yNew, p.priceY(), p.priceX(), y0, p.equilibriumReserve0(), p.concentrationY()) // move to f() - : CurveLib.fInverse(yNew, px, py, x0, y0, p.concentrationX()); + : CurveLib.fInverse(yNew, p.priceX(), p.priceY(), p.equilibriumReserve0(), y0, p.concentrationX()); xNew = xNew.unsafeInc(xNew == 0); return reserve0.saturatingSub(xNew); } @@ -625,31 +622,26 @@ library EulerSwapLib { } uint256 inLimitFromOutLimit; - { - uint256 px = p.priceX(); - uint256 py = p.priceY(); - uint256 x0 = p.equilibriumReserve0(); + if (zeroForOne) { + // swap Y out and X in + uint256 yNew = reserve1.saturatingSub(outLimit); uint256 y0 = p.equilibriumReserve1(); - - if (zeroForOne) { - // swap Y out and X in - uint256 yNew = reserve1.saturatingSub(outLimit); - uint256 xNew = yNew <= y0 - // remain on g() - ? CurveLib.saturatingF(yNew, py, px, y0, x0, p.concentrationY()) - // move to f() - : CurveLib.fInverse(yNew, px, py, x0, y0, p.concentrationX()); - inLimitFromOutLimit = xNew.saturatingSub(reserve0); - } else { - // swap X out and Y in - uint256 xNew = reserve0.saturatingSub(outLimit); - uint256 yNew = xNew <= x0 - // remain on f() - ? CurveLib.saturatingF(xNew, px, py, x0, y0, p.concentrationX()) - // move to g() - : CurveLib.fInverse(xNew, py, px, y0, x0, p.concentrationY()); - inLimitFromOutLimit = yNew.saturatingSub(reserve1); - } + uint256 xNew = yNew <= y0 + // remain on g() + ? CurveLib.saturatingF(yNew, p.priceY(), p.priceX(), y0, p.equilibriumReserve0(), p.concentrationY()) + // move to f() + : CurveLib.fInverse(yNew, p.priceX(), p.priceY(), p.equilibriumReserve0(), y0, p.concentrationX()); + inLimitFromOutLimit = xNew.saturatingSub(reserve0); + } else { + // swap X out and Y in + uint256 xNew = reserve0.saturatingSub(outLimit); + uint256 x0 = p.equilibriumReserve0(); + uint256 yNew = xNew <= x0 + // remain on f() + ? CurveLib.saturatingF(xNew, p.priceX(), p.priceY(), x0, p.equilibriumReserve1(), p.concentrationX()) + // move to g() + : CurveLib.fInverse(xNew, p.priceY(), p.priceX(), p.equilibriumReserve1(), x0, p.concentrationY()); + inLimitFromOutLimit = yNew.saturatingSub(reserve1); } unchecked { From 2a01f7e96a90c6279d0f68d1e1b6c6eec2f7bd82 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Sat, 26 Jul 2025 17:43:54 -0400 Subject: [PATCH 24/25] Stack too deep --- src/core/EulerSwap.sol | 31 +++++++++---------- test/integration/EulerSwap.t.sol | 53 +++----------------------------- 2 files changed, 19 insertions(+), 65 deletions(-) diff --git a/src/core/EulerSwap.sol b/src/core/EulerSwap.sol index eddda897e..c35c1e27b 100644 --- a/src/core/EulerSwap.sol +++ b/src/core/EulerSwap.sol @@ -668,16 +668,12 @@ library EulerSwapLib { } } - function checkSolvency( - IEVC evc, - address account, - address vault0, - address vault1, - bool zeroForOne, - uint256 amountIn, - uint256 amountOut - ) internal view returns (bool) { - IEVault[] memory collaterals = evc.fastGetCollaterals(account); + function checkSolvency(IEVC evc, ParamsLib.Params p, bool zeroForOne, uint256 amountIn, uint256 amountOut) + internal + view + returns (bool) + { + IEVault[] memory collaterals = evc.fastGetCollaterals(p.eulerAccount()); // The EVC enforces that there can be at most 1 controller for an Euler // account. Consequently, there is only 1 vault in which the account can incur debt. If // there is no controller (i.e. no debt) then `debtVault` will be zero. @@ -687,7 +683,7 @@ library EulerSwapLib { uint256 debt; { - IEVault[] memory controllers = evc.fastGetControllers(account); + IEVault[] memory controllers = evc.fastGetControllers(p.eulerAccount()); if (controllers.length > 1) { // Not possible unless we're already inside a EVC batch with deferred checks. An @@ -696,16 +692,16 @@ library EulerSwapLib { } if (controllers.length == 1) { debtVault = controllers.get(controllers.iter()); - debt = debtVault.fastDebtOf(account); + debt = debtVault.fastDebtOf(p.eulerAccount()); } } IEVault sellVault; IEVault buyVault; { - (address sellVault_, address buyVault_) = zeroForOne.maybeSwap(vault1, vault0); - sellVault = IEVault(sellVault_); - buyVault = IEVault(buyVault_); + (IERC20 sellVault_, IERC20 buyVault_) = zeroForOne.maybeSwap(p.vault1(), p.vault0()); + sellVault = IEVault(address(sellVault_)); + buyVault = IEVault(address(buyVault_)); } // `newDebt` is new, underlying-denominated debt in the buy token incurred after the @@ -723,7 +719,7 @@ library EulerSwapLib { // Compute the effect of sending `amountOut` of the buy token to the taker. { - uint256 collateralBalance = buyVault.fastConvertToAssets(buyVault.fastBalanceOf(account)); + uint256 collateralBalance = buyVault.fastConvertToAssets(buyVault.fastBalanceOf(p.eulerAccount())); if (collateralBalance < amountOut) { unchecked { newDebt = amountOut - collateralBalance; @@ -816,7 +812,8 @@ library EulerSwapLib { (EVaultIterator i, EVaultIterator end) = (collaterals.iter(), collaterals.end()); i != end; i = i.next() ) { IEVault collateralVault = collaterals.get(i); - uint256 collateralAmount = collateralVault.fastConvertToAssets(collateralVault.fastBalanceOf(account)); + uint256 collateralAmount = + collateralVault.fastConvertToAssets(collateralVault.fastBalanceOf(p.eulerAccount())); if (collateralVault == sellVault) { unchecked { collateralAmount += newCollateral; diff --git a/test/integration/EulerSwap.t.sol b/test/integration/EulerSwap.t.sol index cfe836073..8b5a68278 100644 --- a/test/integration/EulerSwap.t.sol +++ b/test/integration/EulerSwap.t.sol @@ -184,15 +184,7 @@ abstract contract EulerSwapTest is AllowanceHolderPairTest { (uint256 reserve0, uint256 reserve1) = pool.fastGetReserves(); uint256 amountOut = EulerSwapLib.findCurvePoint(eulerSwapAmount(), true, params, reserve0, reserve1); assertTrue( - EulerSwapLib.checkSolvency( - EVC, - address(params.eulerAccount()), - address(params.vault0()), - address(params.vault1()), - true, - eulerSwapAmount(), - amountOut - ), + EulerSwapLib.checkSolvency(EVC, params, true, eulerSwapAmount(), amountOut), "Account is insolvent after swap" ); } @@ -204,15 +196,7 @@ abstract contract EulerSwapTest is AllowanceHolderPairTest { (uint256 reserve0, uint256 reserve1) = pool.fastGetReserves(); uint256 amountOut = EulerSwapLib.findCurvePoint(eulerSwapAmount(), false, params, reserve0, reserve1); assertTrue( - EulerSwapLib.checkSolvency( - EVC, - address(params.eulerAccount()), - address(params.vault0()), - address(params.vault1()), - false, - eulerSwapAmount(), - amountOut - ), + EulerSwapLib.checkSolvency(EVC, params, false, eulerSwapAmount(), amountOut), "Account is insolvent after swap" ); } @@ -224,15 +208,7 @@ abstract contract EulerSwapTest is AllowanceHolderPairTest { (uint256 reserve0, uint256 reserve1) = pool.fastGetReserves(); (uint256 amountIn, uint256 amountOut) = EulerSwapLib.calcLimits(EVC, pool, true, params, reserve0, reserve1); assertTrue( - EulerSwapLib.checkSolvency( - EVC, - address(params.eulerAccount()), - address(params.vault0()), - address(params.vault1()), - true, - amountIn, - amountOut - ), + EulerSwapLib.checkSolvency(EVC, params, true, amountIn, amountOut), "Account is insolvent after swapping at pool limit" ); } @@ -244,15 +220,7 @@ abstract contract EulerSwapTest is AllowanceHolderPairTest { (uint256 reserve0, uint256 reserve1) = pool.fastGetReserves(); (uint256 amountIn, uint256 amountOut) = EulerSwapLib.calcLimits(EVC, pool, false, params, reserve0, reserve1); assertTrue( - EulerSwapLib.checkSolvency( - EVC, - address(params.eulerAccount()), - address(params.vault0()), - address(params.vault1()), - false, - amountIn, - amountOut - ), + EulerSwapLib.checkSolvency(EVC, params, false, amountIn, amountOut), "Account is insolvent after swapping at pool limit" ); } @@ -289,17 +257,6 @@ abstract contract EulerSwapTest is AllowanceHolderPairTest { oracle.fastGetQuotes(debtVault.fastDebtOf(eulerAccount), debtVault.fastAsset(), unitOfAccount); uint256 amountOut = (collateral - debt * 1e4) / 1e4; - assertFalse( - EulerSwapLib.checkSolvency( - EVC, - address(params.eulerAccount()), - address(params.vault0()), - address(params.vault1()), - true, - 0, - amountOut + 1 - ), - "Account should be insolvent" - ); + assertFalse(EulerSwapLib.checkSolvency(EVC, params, true, 0, amountOut + 1), "Account should be insolvent"); } } From cfab0cd81668eb3d8e503de57bfc52fe2180b3cb Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Sat, 26 Jul 2025 17:46:47 -0400 Subject: [PATCH 25/25] Update snaps --- .forge-snapshots/settler_eulerSwapCustody_USDC-USDT.snap | 2 +- .forge-snapshots/settler_eulerSwap_USDC-USDT.snap | 2 +- .forge-snapshots/settler_eulerSwap_USDT-USDC.snap | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.forge-snapshots/settler_eulerSwapCustody_USDC-USDT.snap b/.forge-snapshots/settler_eulerSwapCustody_USDC-USDT.snap index 1401fc7bf..ed23bb7f6 100644 --- a/.forge-snapshots/settler_eulerSwapCustody_USDC-USDT.snap +++ b/.forge-snapshots/settler_eulerSwapCustody_USDC-USDT.snap @@ -1 +1 @@ -527957 \ No newline at end of file +528002 \ No newline at end of file diff --git a/.forge-snapshots/settler_eulerSwap_USDC-USDT.snap b/.forge-snapshots/settler_eulerSwap_USDC-USDT.snap index d61ad6d62..360c58214 100644 --- a/.forge-snapshots/settler_eulerSwap_USDC-USDT.snap +++ b/.forge-snapshots/settler_eulerSwap_USDC-USDT.snap @@ -1 +1 @@ -556942 \ No newline at end of file +556984 \ No newline at end of file diff --git a/.forge-snapshots/settler_eulerSwap_USDT-USDC.snap b/.forge-snapshots/settler_eulerSwap_USDT-USDC.snap index dc9d9650f..e58a4220e 100644 --- a/.forge-snapshots/settler_eulerSwap_USDT-USDC.snap +++ b/.forge-snapshots/settler_eulerSwap_USDT-USDC.snap @@ -1 +1 @@ -563543 \ No newline at end of file +563557 \ No newline at end of file