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 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 diff --git a/src/core/EulerSwap.sol b/src/core/EulerSwap.sol index 5de1a1445..c35c1e27b 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 { @@ -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 (IEVault[] memory collaterals) { + assembly ("memory-safe") { + mstore(0x14, account) + mstore(0x00, 0xa4d25d1e000000000000000000000000) // selector for `getCollaterals(address)` with `account`'s padding + + if iszero(staticcall(gas(), evc, 0x10, 0x24, 0x00, 0x00)) { + let ptr := mload(0x40) + returndatacopy(ptr, 0x00, returndatasize()) + revert(ptr, returndatasize()) + } + + let size := sub(returndatasize(), 0x20) + collaterals := mload(0x40) + returndatacopy(collaterals, 0x20, size) + mstore(0x40, add(collaterals, size)) + } + } + + 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 + + if iszero(staticcall(gas(), evc, 0x10, 0x24, 0x00, 0x00)) { + let ptr := mload(0x40) + returndatacopy(ptr, 0x00, returndatasize()) + revert(ptr, returndatasize()) + } + + let size := sub(returndatasize(), 0x20) + controllers := mload(0x40) + returndatacopy(controllers, 0x20, size) + mstore(0x40, add(controllers, size)) + } + } } interface IOracle { @@ -78,6 +115,35 @@ interface IOracle { // for computing collateral value, use `bidOutAmount` } +library FastOracle { + 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(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) + } + } +} + 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 +280,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 { @@ -380,113 +487,83 @@ library ParamsLib { } } -abstract contract EulerSwap is SettlerAbstract { - using FastLogic for bool; - using Ternary for bool; - using UnsafeMath for uint256; - using Math for uint256; - using SafeTransferLib for IERC20; - 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); +type EVaultIterator is uint256; - 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 - ); +library LibEVaultArray { + function iter(IEVault[] memory a) internal pure returns (EVaultIterator i) { + assembly ("memory-safe") { + i := add(0x20, a) + } } - 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; - } - 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); + function end(IEVault[] memory a) internal pure returns (EVaultIterator i) { + assembly ("memory-safe") { + i := add(0x20, add(shl(0x05, mload(a)), a)) } + } - // 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); + function next(EVaultIterator i) internal pure returns (EVaultIterator) { + unchecked { + return EVaultIterator.wrap(32 + EVaultIterator.unwrap(i)); } + } - // 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); + 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; + using Ternary for bool; + using SafeTransferLib for IEVault; + using ParamsLib for ParamsLib.Params; + 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) - private + internal 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); } @@ -503,11 +580,14 @@ 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) - private - 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; { @@ -520,7 +600,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 @@ -542,31 +622,26 @@ abstract contract EulerSwap is SettlerAbstract { } 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 { @@ -592,4 +667,261 @@ abstract contract EulerSwap is SettlerAbstract { return (amountCap == 0).ternary(type(uint112).max, 10 ** (amountCap & 63) * (amountCap >> 6) / 100); } } + + 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. + 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; + + { + 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 + // 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(p.eulerAccount()); + } + } + + IEVault sellVault; + 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 + // 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 the effect of sending `amountOut` of the buy token to the taker. + { + uint256 collateralBalance = buyVault.fastConvertToAssets(buyVault.fastBalanceOf(p.eulerAccount())); + if (collateralBalance < amountOut) { + unchecked { + newDebt = amountOut - collateralBalance; + } + soldCollateral = collateralBalance; + } else { + soldCollateral = amountOut; + } + } + + // Compute the effect of receiving `amountIn` of the sell token from the taker. + if (debtVault == sellVault) { + // We are repaying debt; we have to check whether this will cause us to disable + // `sellVault` as the controller. + if (amountIn < debt) { + // 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) { + // 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; + } + } else { + unchecked { + newCollateral = amountIn - debt; + } + } + debt = debt.saturatingSub(amountIn); + } else { + newCollateral = amountIn; + } + + if (newDebt != 0) { + // 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) { + // 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; + } + unchecked { + debt += newDebt; + } + } + + // 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); + // 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`. + 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() + ) { + IEVault collateralVault = collaterals.get(i); + uint256 collateralAmount = + collateralVault.fastConvertToAssets(collateralVault.fastBalanceOf(p.eulerAccount())); + if (collateralVault == sellVault) { + unchecked { + collateralAmount += newCollateral; + } + newCollateral = 0; + } else if (collateralVault == buyVault) { + unchecked { + collateralAmount -= soldCollateral; + } + } + if (collateralAmount != 0) { + (uint256 value,) = + oracle.fastGetQuotes(collateralAmount, collateralVault.fastAsset(), unitOfAccount); + unchecked { + collateral += (value * debtVault.fastLTVBorrow(collateralVault)); + } + if (collateral >= debt) { + return true; + } + } + } + if (newCollateral != 0) { + // 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); + unchecked { + collateral += (value * debtVault.fastLTVBorrow(sellVault)); + } + } + return collateral >= debt; + } else { + return true; + } + } +} + +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; + } + // 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); + } + 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); + } + } } diff --git a/test/integration/EulerSwap.t.sol b/test/integration/EulerSwap.t.sol index aa5c3f749..8b5a68278 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} 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,6 +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); @@ -158,4 +176,87 @@ 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, params, true, eulerSwapAmount(), amountOut), + "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, params, 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, params, 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, params, false, amountIn, amountOut), + "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()); + + IEVault[] memory collaterals = EVC.fastGetCollaterals(eulerAccount); + IEVault[] memory controllers = EVC.fastGetControllers(eulerAccount); + assertEq(controllers.length, 1, "Multiple debt vaults"); + assertEq(address(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.fastGetQuotes( + collateralVault.fastConvertToAssets(collateralVault.fastBalanceOf(eulerAccount)), + collateralVault.fastAsset(), + unitOfAccount + ); + + collateral += (value * debtVault.fastLTVBorrow(collateralVault)); + } + (, uint256 debt) = + oracle.fastGetQuotes(debtVault.fastDebtOf(eulerAccount), debtVault.fastAsset(), unitOfAccount); + uint256 amountOut = (collateral - debt * 1e4) / 1e4; + + assertFalse(EulerSwapLib.checkSolvency(EVC, params, true, 0, amountOut + 1), "Account should be insolvent"); + } }