From a8fd2eb6f6c97b5bae92feda1ef7e3d965d546e1 Mon Sep 17 00:00:00 2001 From: "ultrasecr.eth" <241804+ultrasecreth@users.noreply.github.com> Date: Wed, 24 Apr 2024 18:55:14 +0200 Subject: [PATCH] Use .liquidity method on Uni when filtering pools --- .forge-snapshots/UniswapV3Pendle.snap | 2 +- README.md | 2 +- src/pendle/UniswapV3PendleWrapper.sol | 4 +- src/uniswapV3/UniswapV3Wrapper.sol | 29 +++------ test/UniswapV3PendleWrapper.t.sol | 8 +-- test/UniswapV3Wrapper.LiquidityBug.t.sol | 82 ++++++++++++++++++++++++ 6 files changed, 98 insertions(+), 29 deletions(-) create mode 100644 test/UniswapV3Wrapper.LiquidityBug.t.sol diff --git a/.forge-snapshots/UniswapV3Pendle.snap b/.forge-snapshots/UniswapV3Pendle.snap index ffeee4e..39e1198 100644 --- a/.forge-snapshots/UniswapV3Pendle.snap +++ b/.forge-snapshots/UniswapV3Pendle.snap @@ -1 +1 @@ -497567 \ No newline at end of file +493910 \ No newline at end of file diff --git a/README.md b/README.md index f9c3d4b..26ca0af 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ repayment approval. | Camelot | 0x5E8820B2832aD8451f65Fa2CCe2F3Cef29016D0d | Arbitrum One | 80679 | 0.01% | [AlgebraWrapper](src/algebra/AlgebraWrapper.sol) | | Camelot + Pendle | 0xC9d66F655b7B35A2B4958bE2FB58E472736Bbc47 | Arbitrum One | 506792 | 0.01% | [AlgebraPendleWrapper](src/pendle/AlgebraPendleWrapper.sol) | | Balancer + Pendle | 0xC1Ea6a6df39D991006b39706db7C51f5A1819da7 | Arbitrum One | 525422 | 0 | [BalancerPendleWrapper](src/pendle/BalancerPendleWrapper.sol) | -| Uniswap v3 + Pendle | 0xa353Fd50210786F0E038ddD574A21d0CCefb3163 | Arbitrum One | 497567 | Variable | [UniswapV3PendleWrapper](src/pendle/UniswapV3PendleWrapper.sol) | +| Uniswap v3 + Pendle | 0xd652e854b1a387140889D95beBC9142a0895e667 | Arbitrum One | 497567 | Variable | [UniswapV3PendleWrapper](src/pendle/UniswapV3PendleWrapper.sol) | | Aerodrome | 0x69b6E55f00d908018E2D745c524995bc231D762b | Base | 163919 | Variable | [SolidlyWrapper](src/solidly/SolidlyWrapper.sol) | | Velodrome | 0xcF13CDdbA3aEf757c52466deC310F221e06238d6 | Optimism | 163919 | Variable | [SolidlyWrapper](src/solidly/SolidlyWrapper.sol) | diff --git a/src/pendle/UniswapV3PendleWrapper.sol b/src/pendle/UniswapV3PendleWrapper.sol index 393a6fa..58f9937 100644 --- a/src/pendle/UniswapV3PendleWrapper.sol +++ b/src/pendle/UniswapV3PendleWrapper.sol @@ -125,7 +125,7 @@ contract UniswapV3PendleWrapper is BasePendleWrapper, IUniswapV3FlashCallback, A for (uint256 i = 0; i < 4; i++) { IUniswapV3Pool __pool = _pool(asset, fees[i]); uint256 _balance = __pool.balance(asset); - if (address(__pool) != address(0) && _balance > poolBalance) { + if (address(__pool) != address(0) && __pool.liquidity() > 0 && _balance > poolBalance) { pool = __pool; poolBalance = _balance; poolFee = fees[i]; @@ -140,7 +140,7 @@ contract UniswapV3PendleWrapper is BasePendleWrapper, IUniswapV3FlashCallback, A } function canLoan(IUniswapV3Pool pool, address asset, uint256 amount) view returns (bool) { - return balance(pool, asset) >= amount; + return balance(pool, asset) >= amount && pool.liquidity() > 0; } function balance(IUniswapV3Pool pool, address asset) view returns (uint256) { diff --git a/src/uniswapV3/UniswapV3Wrapper.sol b/src/uniswapV3/UniswapV3Wrapper.sol index 31f03d0..289bfa0 100644 --- a/src/uniswapV3/UniswapV3Wrapper.sol +++ b/src/uniswapV3/UniswapV3Wrapper.sol @@ -25,15 +25,13 @@ contract UniswapV3Wrapper is BaseWrapper, IUniswapV3FlashCallback { // DEFAULT ASSETS address public immutable weth; address public immutable usdc; - address public immutable usdt; /// @param reg Registry storing constructor parameters constructor(Registry reg) { // @param factory_ Uniswap v3 UniswapV3Factory address // @param weth_ Weth contract used in Uniswap v3 Pairs // @param usdc_ usdc contract used in Uniswap v3 Pairs - // @param usdt_ usdt contract used in Uniswap v3 Pairs - (factory, weth, usdc, usdt) = abi.decode(reg.getSafe("UniswapV3Wrapper"), (address, address, address, address)); + (factory, weth, usdc) = abi.decode(reg.getSafe("UniswapV3Wrapper"), (address, address, address)); } /** @@ -44,14 +42,9 @@ contract UniswapV3Wrapper is BaseWrapper, IUniswapV3FlashCallback { * @return pool The Uniswap V3 Pool that will be used as the source of the flash loan. */ function cheapestPool(address asset, uint256 amount) public view returns (IUniswapV3Pool pool) { - // Try a stable pair first - pool = _pool(asset, asset == usdc ? usdt : usdc, 0.0001e6); - if (address(pool) != address(0) && pool.canLoan(asset, amount)) return pool; - - // Look for the cheapest fee otherwise - uint16[3] memory fees = [0.0005e6, 0.003e6, 0.01e6]; address assetOther = asset == weth ? usdc : weth; - for (uint256 i = 0; i < 3; i++) { + uint16[4] memory fees = [0.0001e6, 0.0005e6, 0.003e6, 0.01e6]; + for (uint256 i = 0; i < 4; i++) { pool = _pool(asset, assetOther, fees[i]); if (address(pool) != address(0) && pool.canLoan(asset, amount)) return pool; } @@ -110,18 +103,12 @@ contract UniswapV3Wrapper is BaseWrapper, IUniswapV3FlashCallback { } function _maxFlashLoan(address asset) internal view returns (uint256 max) { - // Try a stable pair first - IUniswapV3Pool pool = _pool(asset, asset == usdc ? usdt : usdc, 0.0001e6); - if (address(pool) != address(0)) { - max = pool.balance(asset); - } - - uint16[3] memory fees = [0.0005e6, 0.003e6, 0.01e6]; address assetOther = asset == weth ? usdc : weth; - for (uint256 i = 0; i < 3; i++) { - pool = _pool(asset, assetOther, fees[i]); + uint16[4] memory fees = [0.0001e6, 0.0005e6, 0.003e6, 0.01e6]; + for (uint256 i = 0; i < 4; i++) { + IUniswapV3Pool pool = _pool(asset, assetOther, fees[i]); uint256 _balance = pool.balance(asset); - if (address(pool) != address(0) && _balance > max) { + if (address(pool) != address(0) && pool.liquidity() > 0 && _balance > max) { max = _balance; } } @@ -135,7 +122,7 @@ contract UniswapV3Wrapper is BaseWrapper, IUniswapV3FlashCallback { } function canLoan(IUniswapV3Pool pool, address asset, uint256 amount) view returns (bool) { - return balance(pool, asset) >= amount; + return balance(pool, asset) >= amount && pool.liquidity() > 0; } function balance(IUniswapV3Pool pool, address asset) view returns (uint256) { diff --git a/test/UniswapV3PendleWrapper.t.sol b/test/UniswapV3PendleWrapper.t.sol index caf52a2..97ff797 100644 --- a/test/UniswapV3PendleWrapper.t.sol +++ b/test/UniswapV3PendleWrapper.t.sol @@ -40,7 +40,7 @@ contract UniswapV3PendleWrapperTest is Test { revert("API_KEY_ALCHEMY variable missing"); } - vm.createSelectFork({ urlOrAlias: "arbitrum_one", blockNumber: 199_563_251 }); + vm.createSelectFork({ urlOrAlias: "arbitrum_one", blockNumber: 204_382_544 }); factory = IUniswapV3Factory(0x1F98431c8aD98523631AE4a59f267346ea31F984); pendleRouter = IPendleRouterV3(0x00000000005BBB0EF59571E58418F9a4357b68A0); weth = 0x82aF49447D8a07e3bd95BD0d56f35241523fBab1; @@ -64,21 +64,21 @@ contract UniswapV3PendleWrapperTest is Test { token = 0x8EA5040d423410f1fdc363379Af88e1DB5eA1C34; // PT-ezETH-27JUN2024 underlying = 0x2416092f143378750bb29b79eD961ab195CcEea5; - test_maxFlashLoan(103.703691885699322816e18); + test_maxFlashLoan(25.571331873721834032e18); } function test_maxFlashLoan_PTeETH() external { token = 0x1c27Ad8a19Ba026ADaBD615F6Bc77158130cfBE4; // PT-eETH-27JUN2024 underlying = 0x35751007a407ca6FEFfE80b3cB397736D2cf4dbe; - test_maxFlashLoan(116.621531724330957646e18); + test_maxFlashLoan(278.935838876776410741e18); } function test_maxFlashLoan_PTrsETH() external { token = 0xAFD22F824D51Fb7EeD4778d303d4388AC644b026; // PT-rsETH-27JUN2024 underlying = 0x4186BFC76E2E237523CBC30FD220FE055156b41F; - test_maxFlashLoan(8.816052000596960554e18); + test_maxFlashLoan(8.986835885934438101e18); } function test_maxFlashLoan(uint256 expected) internal { diff --git a/test/UniswapV3Wrapper.LiquidityBug.t.sol b/test/UniswapV3Wrapper.LiquidityBug.t.sol new file mode 100644 index 0000000..d24c21d --- /dev/null +++ b/test/UniswapV3Wrapper.LiquidityBug.t.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.19 <0.9.0; + +import { Test } from "forge-std/Test.sol"; +import { console2 } from "forge-std/console2.sol"; +import { StdCheats } from "forge-std/StdCheats.sol"; + +import { IERC20Metadata as IERC20 } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import { Registry } from "src/Registry.sol"; + +import { MockBorrower } from "./MockBorrower.sol"; +import { UniswapV3Wrapper } from "../src/uniswapV3/UniswapV3Wrapper.sol"; +import { IUniswapV3Factory } from "../src/uniswapV3/interfaces/IUniswapV3Factory.sol"; +import { Arrays } from "src/utils/Arrays.sol"; + +/// @dev If this is your first time with Forge, read this tutorial in the Foundry Book: +/// https://book.getfoundry.sh/forge/writing-tests +contract UniswapV3WrapperLiquidityTest is Test { + using Arrays for *; + + UniswapV3Wrapper internal wrapper; + MockBorrower internal borrower; + address internal usdc; + address internal usdt; + address internal weth; + address internal rseth; + IUniswapV3Factory internal factory; + + /// @dev A function invoked before each test case is run. + function setUp() public virtual { + // Revert if there is no API key. + string memory alchemyApiKey = vm.envOr("API_KEY_ALCHEMY", string("")); + if (bytes(alchemyApiKey).length == 0) { + revert("API_KEY_ALCHEMY variable missing"); + } + + vm.createSelectFork({ urlOrAlias: "arbitrum_one", blockNumber: 204_382_544 }); + factory = IUniswapV3Factory(0x1F98431c8aD98523631AE4a59f267346ea31F984); + usdc = 0xaf88d065e77c8cC2239327C5EDb3A432268e5831; + weth = 0x82aF49447D8a07e3bd95BD0d56f35241523fBab1; + rseth = 0x4186BFC76E2E237523CBC30FD220FE055156b41F; + + Registry registry = new Registry(address(this).toArray(), address(this).toArray()); + registry.set("UniswapV3Wrapper", abi.encode(address(factory), weth, usdc)); + wrapper = new UniswapV3Wrapper(registry); + borrower = new MockBorrower(wrapper); + } + + /// @dev Basic test. Run it with `forge test -vvv` to see the console log. + function test_flashFee() external { + console2.log("test_flashFee"); + assertEqDecimal(wrapper.flashFee(rseth, 1e18), 0.003e18, 18, "Fee not exact"); + } + + function test_maxFlashLoan() external { + console2.log("test_maxFlashLoan"); + assertEqDecimal(wrapper.maxFlashLoan(rseth), 8.986835885934438101e18, 18, "Max flash loan not right"); + } + + function test_flashLoan_RSETH() external { + test_flashLoan(rseth, 5e18); + } + + function test_flashLoan(address token, uint256 loan) internal { + console2.log(string.concat("test_flashLoan: ", IERC20(token).symbol())); + uint256 fee = wrapper.flashFee(token, loan); + deal(address(token), address(borrower), fee); + bytes memory result = borrower.flashBorrow(token, loan); + + // Test the return values passed through the wrapper + (bytes32 callbackReturn) = abi.decode(result, (bytes32)); + assertEq(uint256(callbackReturn), uint256(borrower.ERC3156PP_CALLBACK_SUCCESS()), "Callback failed"); + + // Test the borrower state during the callback + assertEq(borrower.flashInitiator(), address(borrower), "flashInitiator"); + assertEq(address(borrower.flashAsset()), address(token), "flashAsset"); + assertEq(borrower.flashAmount(), loan, "flashAmount"); + // The amount we transferred to pay for fees, plus the amount we borrowed + assertEq(borrower.flashBalance(), loan + fee, "flashBalance"); + assertEq(borrower.flashFee(), fee, "flashFee"); + } +}