From 8e20594156d17f8b25081df91086917301938621 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Tue, 2 Jul 2024 13:59:27 -0400 Subject: [PATCH 01/21] feat: update swap to swapExactAmountIn --- src/PSM3.sol | 2 +- src/interfaces/IPSM3.sol | 10 +++---- test/invariant/handlers/SwapperHandler.sol | 4 +-- test/unit/Events.t.sol | 2 +- test/unit/Swaps.t.sol | 32 +++++++++++----------- 5 files changed, 25 insertions(+), 25 deletions(-) diff --git a/src/PSM3.sol b/src/PSM3.sol index de8946a..676faa0 100644 --- a/src/PSM3.sol +++ b/src/PSM3.sol @@ -56,7 +56,7 @@ contract PSM3 is IPSM3 { /*** Swap functions ***/ /**********************************************************************************************/ - function swap( + function swapExactIn( address assetIn, address assetOut, uint256 amountIn, diff --git a/src/interfaces/IPSM3.sol b/src/interfaces/IPSM3.sol index 06adef4..c6574cd 100644 --- a/src/interfaces/IPSM3.sol +++ b/src/interfaces/IPSM3.sol @@ -113,10 +113,10 @@ interface IPSM3 { /**********************************************************************************************/ /** - * @dev Swaps an amount of assetIn for assetOut in the PSM. The amount swapped is converted - * based on the current value of the two assets used in the swap. This function will - * revert if there is not enough balance in the PSM to facilitate the swap. Both assets - * must be supported in the PSM in order to succeed. + * @dev Swaps a specified amount of assetIn for assetOut in the PSM. The amount swapped is + * converted based on the current value of the two assets used in the swap. This + * function will revert if there is not enough balance in the PSM to facilitate the + * swap. Both assets must be supported in the PSM in order to succeed. * @param assetIn Address of the ERC-20 asset to swap in. * @param assetOut Address of the ERC-20 asset to swap out. * @param amountIn Amount of the asset to swap in. @@ -124,7 +124,7 @@ interface IPSM3 { * @param receiver Address of the receiver of the swapped assets. * @param referralCode Referral code for the swap. */ - function swap( + function swapExactIn( address assetIn, address assetOut, uint256 amountIn, diff --git a/test/invariant/handlers/SwapperHandler.sol b/test/invariant/handlers/SwapperHandler.sol index 5fcbfeb..86c1525 100644 --- a/test/invariant/handlers/SwapperHandler.sol +++ b/test/invariant/handlers/SwapperHandler.sol @@ -30,7 +30,7 @@ contract SwapperHandler is HandlerBase { return swappers[indexSeed % swappers.length]; } - function swap( + function swapExactIn( uint256 assetInSeed, uint256 assetOutSeed, uint256 swapperSeed, @@ -77,7 +77,7 @@ contract SwapperHandler is HandlerBase { vm.startPrank(swapper); assetIn.mint(swapper, amountIn); assetIn.approve(address(psm), amountIn); - psm.swap(address(assetIn), address(assetOut), amountIn, minAmountOut, swapper, 0); + psm.swapExactIn(address(assetIn), address(assetOut), amountIn, minAmountOut, swapper, 0); vm.stopPrank(); swapCount++; diff --git a/test/unit/Events.t.sol b/test/unit/Events.t.sol index 853b576..5611bfe 100644 --- a/test/unit/Events.t.sol +++ b/test/unit/Events.t.sol @@ -110,6 +110,6 @@ contract PSMEventTests is PSMTestBase { vm.expectEmit(address(psm)); emit Swap(assetIn, assetOut, sender, receiver, amountIn, expectedAmountOut, referralCode); - psm.swap(assetIn, assetOut, amountIn, 0, receiver, referralCode); + psm.swapExactIn(assetIn, assetOut, amountIn, 0, receiver, referralCode); } } diff --git a/test/unit/Swaps.t.sol b/test/unit/Swaps.t.sol index 43e4bbd..0e779a7 100644 --- a/test/unit/Swaps.t.sol +++ b/test/unit/Swaps.t.sol @@ -22,37 +22,37 @@ contract PSMSwapFailureTests is PSMTestBase { function test_swap_amountZero() public { vm.expectRevert("PSM3/invalid-amountIn"); - psm.swap(address(usdc), address(sDai), 0, 0, receiver, 0); + psm.swapExactIn(address(usdc), address(sDai), 0, 0, receiver, 0); } function test_swap_receiverZero() public { vm.expectRevert("PSM3/invalid-receiver"); - psm.swap(address(usdc), address(sDai), 100e6, 80e18, address(0), 0); + psm.swapExactIn(address(usdc), address(sDai), 100e6, 80e18, address(0), 0); } function test_swap_invalid_assetIn() public { vm.expectRevert("PSM3/invalid-asset"); - psm.swap(makeAddr("other-token"), address(sDai), 100e6, 80e18, receiver, 0); + psm.swapExactIn(makeAddr("other-token"), address(sDai), 100e6, 80e18, receiver, 0); } function test_swap_invalid_assetOut() public { vm.expectRevert("PSM3/invalid-asset"); - psm.swap(address(usdc), makeAddr("other-token"), 100e6, 80e18, receiver, 0); + psm.swapExactIn(address(usdc), makeAddr("other-token"), 100e6, 80e18, receiver, 0); } function test_swap_bothAsset0() public { vm.expectRevert("PSM3/invalid-asset"); - psm.swap(address(dai), address(dai), 100e6, 80e18, receiver, 0); + psm.swapExactIn(address(dai), address(dai), 100e6, 80e18, receiver, 0); } function test_swap_bothAsset1() public { vm.expectRevert("PSM3/invalid-asset"); - psm.swap(address(usdc), address(usdc), 100e6, 80e18, receiver, 0); + psm.swapExactIn(address(usdc), address(usdc), 100e6, 80e18, receiver, 0); } function test_swap_bothAsset2() public { vm.expectRevert("PSM3/invalid-asset"); - psm.swap(address(sDai), address(sDai), 100e6, 80e18, receiver, 0); + psm.swapExactIn(address(sDai), address(sDai), 100e6, 80e18, receiver, 0); } function test_swap_minAmountOutBoundary() public { @@ -67,9 +67,9 @@ contract PSMSwapFailureTests is PSMTestBase { assertEq(expectedAmountOut, 80e18); vm.expectRevert("PSM3/amountOut-too-low"); - psm.swap(address(usdc), address(sDai), 100e6, 80e18 + 1, receiver, 0); + psm.swapExactIn(address(usdc), address(sDai), 100e6, 80e18 + 1, receiver, 0); - psm.swap(address(usdc), address(sDai), 100e6, 80e18, receiver, 0); + psm.swapExactIn(address(usdc), address(sDai), 100e6, 80e18, receiver, 0); } function test_swap_insufficientApproveBoundary() public { @@ -80,11 +80,11 @@ contract PSMSwapFailureTests is PSMTestBase { usdc.approve(address(psm), 100e6 - 1); vm.expectRevert("SafeERC20/transfer-from-failed"); - psm.swap(address(usdc), address(sDai), 100e6, 80e18, receiver, 0); + psm.swapExactIn(address(usdc), address(sDai), 100e6, 80e18, receiver, 0); usdc.approve(address(psm), 100e6); - psm.swap(address(usdc), address(sDai), 100e6, 80e18, receiver, 0); + psm.swapExactIn(address(usdc), address(sDai), 100e6, 80e18, receiver, 0); } function test_swap_insufficientUserBalanceBoundary() public { @@ -95,11 +95,11 @@ contract PSMSwapFailureTests is PSMTestBase { usdc.approve(address(psm), 100e6); vm.expectRevert("SafeERC20/transfer-from-failed"); - psm.swap(address(usdc), address(sDai), 100e6, 80e18, receiver, 0); + psm.swapExactIn(address(usdc), address(sDai), 100e6, 80e18, receiver, 0); usdc.mint(swapper, 1); - psm.swap(address(usdc), address(sDai), 100e6, 80e18, receiver, 0); + psm.swapExactIn(address(usdc), address(sDai), 100e6, 80e18, receiver, 0); } function test_swap_insufficientPsmBalanceBoundary() public { @@ -117,9 +117,9 @@ contract PSMSwapFailureTests is PSMTestBase { assertEq(expectedAmountOut, 100.000001e18); // More than balance of sDAI vm.expectRevert("SafeERC20/transfer-failed"); - psm.swap(address(usdc), address(sDai), 125e6 + 2, 100e18, receiver, 0); + psm.swapExactIn(address(usdc), address(sDai), 125e6 + 2, 100e18, receiver, 0); - psm.swap(address(usdc), address(sDai), 125e6, 100e18, receiver, 0); + psm.swapExactIn(address(usdc), address(sDai), 125e6, 100e18, receiver, 0); } } @@ -165,7 +165,7 @@ contract PSMSwapSuccessTestsBase is PSMTestBase { assertEq(assetOut.balanceOf(receiver_), 0); assertEq(assetOut.balanceOf(address(psm)), psmAssetOutBalance); - psm.swap(address(assetIn), address(assetOut), amountIn, amountOut, receiver_, 0); + psm.swapExactIn(address(assetIn), address(assetOut), amountIn, amountOut, receiver_, 0); assertEq(assetIn.allowance(swapper_, address(psm)), 0); From b3a2d672b51eced0f60e39d84a988461b9a84dcd Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Tue, 2 Jul 2024 14:36:53 -0400 Subject: [PATCH 02/21] feat: add preview test coverage --- src/PSM3.sol | 89 +++++--- src/interfaces/IPSM3.sol | 6 +- test/invariant/handlers/SwapperHandler.sol | 4 +- test/unit/Previews.t.sol | 236 ++++++++++++++++----- test/unit/Swaps.t.sol | 4 +- 5 files changed, 248 insertions(+), 91 deletions(-) diff --git a/src/PSM3.sol b/src/PSM3.sol index 676faa0..8d49b50 100644 --- a/src/PSM3.sol +++ b/src/PSM3.sol @@ -69,7 +69,7 @@ contract PSM3 is IPSM3 { require(amountIn != 0, "PSM3/invalid-amountIn"); require(receiver != address(0), "PSM3/invalid-receiver"); - uint256 amountOut = previewSwap(assetIn, assetOut, amountIn); + uint256 amountOut = previewSwapExactIn(assetIn, assetOut, amountIn); require(amountOut >= minAmountOut, "PSM3/amountOut-too-low"); @@ -157,25 +157,16 @@ contract PSM3 is IPSM3 { /*** Swap preview functions ***/ /**********************************************************************************************/ - function previewSwap(address assetIn, address assetOut, uint256 amountIn) + function previewSwapExactIn(address assetIn, address assetOut, uint256 amountIn) public view override returns (uint256 amountOut) { - if (assetIn == address(asset0)) { - if (assetOut == address(asset1)) return _previewOneToOneSwap(amountIn, _asset0Precision, _asset1Precision); - else if (assetOut == address(asset2)) return _previewSwapToAsset2(amountIn, _asset0Precision); - } - - else if (assetIn == address(asset1)) { - if (assetOut == address(asset0)) return _previewOneToOneSwap(amountIn, _asset1Precision, _asset0Precision); - else if (assetOut == address(asset2)) return _previewSwapToAsset2(amountIn, _asset1Precision); - } - - else if (assetIn == address(asset2)) { - if (assetOut == address(asset0)) return _previewSwapFromAsset2(amountIn, _asset0Precision); - else if (assetOut == address(asset1)) return _previewSwapFromAsset2(amountIn, _asset1Precision); - } + amountOut = _getSwapQuote(assetIn, assetOut, amountIn); + } - revert("PSM3/invalid-asset"); + function previewSwapExactOut(address assetIn, address assetOut, uint256 amountOut) + public view override returns (uint256 amountIn) + { + amountIn = _getSwapQuote(assetOut, assetIn, amountOut); } /**********************************************************************************************/ @@ -232,23 +223,9 @@ contract PSM3 is IPSM3 { } /**********************************************************************************************/ - /*** Internal helper functions ***/ + /*** Internal valuation functions (deposit/withdraw) ***/ /**********************************************************************************************/ - function _convertToSharesRoundUp(uint256 assetValue) internal view returns (uint256) { - uint256 totalValue = getPsmTotalValue(); - if (totalValue != 0) { - return _divUp(assetValue * totalShares, totalValue); - } - return assetValue; - } - - function _divUp(uint256 x, uint256 y) internal pure returns (uint256 z) { - unchecked { - z = x != 0 ? ((x - 1) / y) + 1 : 0; - } - } - function _getAssetValue(address asset, uint256 amount) internal view returns (uint256) { if (asset == address(asset0)) return _getAsset0Value(amount); else if (asset == address(asset1)) return _getAsset1Value(amount); @@ -272,10 +249,32 @@ contract PSM3 is IPSM3 { / _asset2Precision; } - function _isValidAsset(address asset) internal view returns (bool) { - return asset == address(asset0) || asset == address(asset1) || asset == address(asset2); + /**********************************************************************************************/ + /*** Internal preview functions (swaps) ***/ + /**********************************************************************************************/ + + function _getSwapQuote(address asset, address quoteAsset, uint256 amount) + public view returns (uint256 quoteAmount) + { + if (asset == address(asset0)) { + if (quoteAsset == address(asset1)) return _previewOneToOneSwap(amount, _asset0Precision, _asset1Precision); + else if (quoteAsset == address(asset2)) return _previewSwapToAsset2(amount, _asset0Precision); + } + + else if (asset == address(asset1)) { + if (quoteAsset == address(asset0)) return _previewOneToOneSwap(amount, _asset1Precision, _asset0Precision); + else if (quoteAsset == address(asset2)) return _previewSwapToAsset2(amount, _asset1Precision); + } + + else if (asset == address(asset2)) { + if (quoteAsset == address(asset0)) return _previewSwapFromAsset2(amount, _asset0Precision); + else if (quoteAsset == address(asset1)) return _previewSwapFromAsset2(amount, _asset1Precision); + } + + revert("PSM3/invalid-asset"); } + function _previewSwapToAsset2(uint256 amountIn, uint256 assetInPrecision) internal view returns (uint256) { @@ -308,4 +307,26 @@ contract PSM3 is IPSM3 { / assetInPrecision; } + /**********************************************************************************************/ + /*** Internal helper functions ***/ + /**********************************************************************************************/ + + function _convertToSharesRoundUp(uint256 assetValue) internal view returns (uint256) { + uint256 totalValue = getPsmTotalValue(); + if (totalValue != 0) { + return _divUp(assetValue * totalShares, totalValue); + } + return assetValue; + } + + function _divUp(uint256 x, uint256 y) internal pure returns (uint256 z) { + unchecked { + z = x != 0 ? ((x - 1) / y) + 1 : 0; + } + } + + function _isValidAsset(address asset) internal view returns (bool) { + return asset == address(asset0) || asset == address(asset1) || asset == address(asset2); + } + } diff --git a/src/interfaces/IPSM3.sol b/src/interfaces/IPSM3.sol index c6574cd..45a1f64 100644 --- a/src/interfaces/IPSM3.sol +++ b/src/interfaces/IPSM3.sol @@ -205,9 +205,13 @@ interface IPSM3 { * @param amountIn Amount of the asset to swap in. * @return amountOut Amount of the asset that will be received in the swap. */ - function previewSwap(address assetIn, address assetOut, uint256 amountIn) + function previewSwapExactIn(address assetIn, address assetOut, uint256 amountIn) external view returns (uint256 amountOut); + // TODO + function previewSwapExactOut(address assetIn, address assetOut, uint256 amountOut) + external view returns (uint256 amountIn); + /**********************************************************************************************/ /*** Conversion functions ***/ /**********************************************************************************************/ diff --git a/test/invariant/handlers/SwapperHandler.sol b/test/invariant/handlers/SwapperHandler.sol index 86c1525..cc827aa 100644 --- a/test/invariant/handlers/SwapperHandler.sol +++ b/test/invariant/handlers/SwapperHandler.sol @@ -53,7 +53,7 @@ contract SwapperHandler is HandlerBase { // By calculating the amount of assetIn we can get from the max asset out, we can // determine the max amount of assetIn we can swap since its the same both ways. - uint256 maxAmountIn = psm.previewSwap( + uint256 maxAmountIn = psm.previewSwapExactIn( address(assetOut), address(assetIn), assetOut.balanceOf(address(psm)) @@ -71,7 +71,7 @@ contract SwapperHandler is HandlerBase { minAmountOut = _bound( minAmountOut, 0, - psm.previewSwap(address(assetIn), address(assetOut), amountIn) + psm.previewSwapExactIn(address(assetIn), address(assetOut), amountIn) ); vm.startPrank(swapper); diff --git a/test/unit/Previews.t.sol b/test/unit/Previews.t.sol index 13d2557..4e0c358 100644 --- a/test/unit/Previews.t.sol +++ b/test/unit/Previews.t.sol @@ -5,59 +5,88 @@ import "forge-std/Test.sol"; import { PSMTestBase } from "test/PSMTestBase.sol"; -contract PSMPreviewSwapFailureTests is PSMTestBase { +contract PSMPreviewSwapExactIn_FailureTests is PSMTestBase { - function test_previewSwap_invalidAssetIn() public { + function test_previewSwapExactIn_invalidAssetIn() public { vm.expectRevert("PSM3/invalid-asset"); - psm.previewSwap(makeAddr("other-token"), address(usdc), 1); + psm.previewSwapExactIn(makeAddr("other-token"), address(usdc), 1); } - function test_previewSwap_invalidAssetOut() public { + function test_previewSwapExactIn_invalidAssetOut() public { vm.expectRevert("PSM3/invalid-asset"); - psm.previewSwap(address(usdc), makeAddr("other-token"), 1); + psm.previewSwapExactIn(address(usdc), makeAddr("other-token"), 1); } - function test_previewSwap_bothAsset0() public { + function test_previewSwapExactIn_bothAsset0() public { vm.expectRevert("PSM3/invalid-asset"); - psm.previewSwap(address(dai), address(dai), 1); + psm.previewSwapExactIn(address(dai), address(dai), 1); } - function test_previewSwap_bothAsset1() public { + function test_previewSwapExactIn_bothAsset1() public { vm.expectRevert("PSM3/invalid-asset"); - psm.previewSwap(address(usdc), address(usdc), 1); + psm.previewSwapExactIn(address(usdc), address(usdc), 1); } - function test_previewSwap_bothAsset2() public { + function test_previewSwapExactIn_bothAsset2() public { vm.expectRevert("PSM3/invalid-asset"); - psm.previewSwap(address(sDai), address(sDai), 1); + psm.previewSwapExactIn(address(sDai), address(sDai), 1); } } -contract PSMPreviewSwapDaiAssetInTests is PSMTestBase { +contract PSMPreviewSwapExactOut_FailureTests is PSMTestBase { - function test_previewSwap_daiToUsdc() public view { - assertEq(psm.previewSwap(address(dai), address(usdc), 1e12 - 1), 0); - assertEq(psm.previewSwap(address(dai), address(usdc), 1e12), 1); + function test_previewSwapExactIn_invalidAssetIn() public { + vm.expectRevert("PSM3/invalid-asset"); + psm.previewSwapExactOut(makeAddr("other-token"), address(usdc), 1); + } + + function test_previewSwapExactOut_invalidAssetOut() public { + vm.expectRevert("PSM3/invalid-asset"); + psm.previewSwapExactOut(address(usdc), makeAddr("other-token"), 1); + } + + function test_previewSwapExactOut_bothAsset0() public { + vm.expectRevert("PSM3/invalid-asset"); + psm.previewSwapExactOut(address(dai), address(dai), 1); + } + + function test_previewSwapExactOut_bothAsset1() public { + vm.expectRevert("PSM3/invalid-asset"); + psm.previewSwapExactOut(address(usdc), address(usdc), 1); + } + + function test_previewSwapExactOut_bothAsset2() public { + vm.expectRevert("PSM3/invalid-asset"); + psm.previewSwapExactOut(address(sDai), address(sDai), 1); + } + +} + +contract PSMPreviewSwapExactIn_DaiAssetInTests is PSMTestBase { + + function test_previewSwapExactIn_daiToUsdc() public view { + assertEq(psm.previewSwapExactIn(address(dai), address(usdc), 1e12 - 1), 0); + assertEq(psm.previewSwapExactIn(address(dai), address(usdc), 1e12), 1); - assertEq(psm.previewSwap(address(dai), address(usdc), 1e18), 1e6); - assertEq(psm.previewSwap(address(dai), address(usdc), 2e18), 2e6); - assertEq(psm.previewSwap(address(dai), address(usdc), 3e18), 3e6); + assertEq(psm.previewSwapExactIn(address(dai), address(usdc), 1e18), 1e6); + assertEq(psm.previewSwapExactIn(address(dai), address(usdc), 2e18), 2e6); + assertEq(psm.previewSwapExactIn(address(dai), address(usdc), 3e18), 3e6); } - function testFuzz_previewSwap_daiToUsdc(uint256 amountIn) public view { + function testFuzz_previewSwapExactIn_daiToUsdc(uint256 amountIn) public view { amountIn = _bound(amountIn, 0, DAI_TOKEN_MAX); - assertEq(psm.previewSwap(address(dai), address(usdc), amountIn), amountIn / 1e12); + assertEq(psm.previewSwapExactIn(address(dai), address(usdc), amountIn), amountIn / 1e12); } - function test_previewSwap_daiToSDai() public view { - assertEq(psm.previewSwap(address(dai), address(sDai), 1e18), 0.8e18); - assertEq(psm.previewSwap(address(dai), address(sDai), 2e18), 1.6e18); - assertEq(psm.previewSwap(address(dai), address(sDai), 3e18), 2.4e18); + function test_previewSwapExactIn_daiToSDai() public view { + assertEq(psm.previewSwapExactIn(address(dai), address(sDai), 1e18), 0.8e18); + assertEq(psm.previewSwapExactIn(address(dai), address(sDai), 2e18), 1.6e18); + assertEq(psm.previewSwapExactIn(address(dai), address(sDai), 3e18), 2.4e18); } - function testFuzz_previewSwap_daiToSDai(uint256 amountIn, uint256 conversionRate) public { + function testFuzz_previewSwapExactIn_daiToSDai(uint256 amountIn, uint256 conversionRate) public { amountIn = _bound(amountIn, 1, DAI_TOKEN_MAX); conversionRate = _bound(conversionRate, 0.0001e27, 1000e27); // 0.01% to 100,000% conversion rate @@ -65,32 +94,65 @@ contract PSMPreviewSwapDaiAssetInTests is PSMTestBase { uint256 amountOut = amountIn * 1e27 / conversionRate; - assertEq(psm.previewSwap(address(dai), address(sDai), amountIn), amountOut); + assertEq(psm.previewSwapExactIn(address(dai), address(sDai), amountIn), amountOut); } } -contract PSMPreviewSwapUSDCAssetInTests is PSMTestBase { +contract PSMPreviewSwapExactOut_DaiAssetInTests is PSMTestBase { - function test_previewSwap_usdcToDai() public view { - assertEq(psm.previewSwap(address(usdc), address(dai), 1e6), 1e18); - assertEq(psm.previewSwap(address(usdc), address(dai), 2e6), 2e18); - assertEq(psm.previewSwap(address(usdc), address(dai), 3e6), 3e18); + function test_previewSwapExactOut_daiToUsdc() public view { + assertEq(psm.previewSwapExactOut(address(dai), address(usdc), 1e6), 1e18); + assertEq(psm.previewSwapExactOut(address(dai), address(usdc), 2e6), 2e18); + assertEq(psm.previewSwapExactOut(address(dai), address(usdc), 3e6), 3e18); } - function testFuzz_previewSwap_usdcToDai(uint256 amountIn) public view { + function testFuzz_previewSwapExactOut_daiToUsdc(uint256 amountOut) public view { + amountOut = _bound(amountOut, 0, USDC_TOKEN_MAX); + + assertEq(psm.previewSwapExactOut(address(dai), address(usdc), amountOut), amountOut * 1e12); + } + + function test_previewSwapExactOut_daiToSDai() public view { + assertEq(psm.previewSwapExactOut(address(dai), address(sDai), 0.8e18), 1e18); + assertEq(psm.previewSwapExactOut(address(dai), address(sDai), 1.6e18), 2e18); + assertEq(psm.previewSwapExactOut(address(dai), address(sDai), 2.4e18), 3e18); + } + + function testFuzz_previewSwapExactOut_daiToSDai(uint256 amountOut, uint256 conversionRate) public { + amountOut = _bound(amountOut, 1, USDC_TOKEN_MAX); + conversionRate = _bound(conversionRate, 0.0001e27, 1000e27); // 0.01% to 100,000% conversion rate + + rateProvider.__setConversionRate(conversionRate); + + uint256 amountIn = amountOut * conversionRate / 1e27; + + assertEq(psm.previewSwapExactOut(address(dai), address(sDai), amountOut), amountIn); + } + +} + +contract PSMPreviewSwapExactIn_USDCAssetInTests is PSMTestBase { + + function test_previewSwapExactIn_usdcToDai() public view { + assertEq(psm.previewSwapExactIn(address(usdc), address(dai), 1e6), 1e18); + assertEq(psm.previewSwapExactIn(address(usdc), address(dai), 2e6), 2e18); + assertEq(psm.previewSwapExactIn(address(usdc), address(dai), 3e6), 3e18); + } + + function testFuzz_previewSwapExactIn_usdcToDai(uint256 amountIn) public view { amountIn = _bound(amountIn, 0, USDC_TOKEN_MAX); - assertEq(psm.previewSwap(address(usdc), address(dai), amountIn), amountIn * 1e12); + assertEq(psm.previewSwapExactIn(address(usdc), address(dai), amountIn), amountIn * 1e12); } - function test_previewSwap_usdcToSDai() public view { - assertEq(psm.previewSwap(address(usdc), address(sDai), 1e6), 0.8e18); - assertEq(psm.previewSwap(address(usdc), address(sDai), 2e6), 1.6e18); - assertEq(psm.previewSwap(address(usdc), address(sDai), 3e6), 2.4e18); + function test_previewSwapExactIn_usdcToSDai() public view { + assertEq(psm.previewSwapExactIn(address(usdc), address(sDai), 1e6), 0.8e18); + assertEq(psm.previewSwapExactIn(address(usdc), address(sDai), 2e6), 1.6e18); + assertEq(psm.previewSwapExactIn(address(usdc), address(sDai), 3e6), 2.4e18); } - function testFuzz_previewSwap_usdcToSDai(uint256 amountIn, uint256 conversionRate) public { + function testFuzz_previewSwapExactIn_usdcToSDai(uint256 amountIn, uint256 conversionRate) public { amountIn = _bound(amountIn, 1, USDC_TOKEN_MAX); conversionRate = _bound(conversionRate, 0.0001e27, 1000e27); // 0.01% to 100,000% conversion rate @@ -98,20 +160,53 @@ contract PSMPreviewSwapUSDCAssetInTests is PSMTestBase { uint256 amountOut = amountIn * 1e27 / conversionRate * 1e12; - assertEq(psm.previewSwap(address(usdc), address(sDai), amountIn), amountOut); + assertEq(psm.previewSwapExactIn(address(usdc), address(sDai), amountIn), amountOut); } } -contract PSMPreviewSwapSDaiAssetInTests is PSMTestBase { +contract PSMPreviewSwapExactOut_USDCAssetInTests is PSMTestBase { - function test_previewSwap_sDaiToDai() public view { - assertEq(psm.previewSwap(address(sDai), address(dai), 1e18), 1.25e18); - assertEq(psm.previewSwap(address(sDai), address(dai), 2e18), 2.5e18); - assertEq(psm.previewSwap(address(sDai), address(dai), 3e18), 3.75e18); + function test_previewSwapExactOut_usdcToDai() public view { + assertEq(psm.previewSwapExactOut(address(usdc), address(dai), 1e18), 1e6); + assertEq(psm.previewSwapExactOut(address(usdc), address(dai), 2e18), 2e6); + assertEq(psm.previewSwapExactOut(address(usdc), address(dai), 3e18), 3e6); } - function testFuzz_previewSwap_sDaiToDai(uint256 amountIn, uint256 conversionRate) public { + function testFuzz_previewSwapExactOut_usdcToDai(uint256 amountOut) public view { + amountOut = _bound(amountOut, 0, DAI_TOKEN_MAX); + + assertEq(psm.previewSwapExactOut(address(usdc), address(dai), amountOut), amountOut / 1e12); + } + + function test_previewSwapExactOut_usdcToSDai() public view { + assertEq(psm.previewSwapExactOut(address(usdc), address(sDai), 0.8e18), 1e6); + assertEq(psm.previewSwapExactOut(address(usdc), address(sDai), 1.6e18), 2e6); + assertEq(psm.previewSwapExactOut(address(usdc), address(sDai), 2.4e18), 3e6); + } + + function testFuzz_previewSwapExactOut_usdcToSDai(uint256 amountOut, uint256 conversionRate) public { + amountOut = _bound(amountOut, 1, SDAI_TOKEN_MAX); + conversionRate = _bound(conversionRate, 0.0001e27, 1000e27); // 0.01% to 100,000% conversion rate + + rateProvider.__setConversionRate(conversionRate); + + uint256 amountIn = amountOut * conversionRate / 1e27 / 1e12; + + assertEq(psm.previewSwapExactOut(address(usdc), address(sDai), amountOut), amountIn); + } + +} + +contract PSMPreviewSwapExactIn_SDaiAssetInTests is PSMTestBase { + + function test_previewSwapExactIn_sDaiToDai() public view { + assertEq(psm.previewSwapExactIn(address(sDai), address(dai), 1e18), 1.25e18); + assertEq(psm.previewSwapExactIn(address(sDai), address(dai), 2e18), 2.5e18); + assertEq(psm.previewSwapExactIn(address(sDai), address(dai), 3e18), 3.75e18); + } + + function testFuzz_previewSwapExactIn_sDaiToDai(uint256 amountIn, uint256 conversionRate) public { amountIn = _bound(amountIn, 1, SDAI_TOKEN_MAX); conversionRate = _bound(conversionRate, 0.0001e27, 1000e27); // 0.01% to 100,000% conversion rate @@ -119,16 +214,16 @@ contract PSMPreviewSwapSDaiAssetInTests is PSMTestBase { uint256 amountOut = amountIn * conversionRate / 1e27; - assertEq(psm.previewSwap(address(sDai), address(dai), amountIn), amountOut); + assertEq(psm.previewSwapExactIn(address(sDai), address(dai), amountIn), amountOut); } - function test_previewSwap_sDaiToUsdc() public view { - assertEq(psm.previewSwap(address(sDai), address(usdc), 1e18), 1.25e6); - assertEq(psm.previewSwap(address(sDai), address(usdc), 2e18), 2.5e6); - assertEq(psm.previewSwap(address(sDai), address(usdc), 3e18), 3.75e6); + function test_previewSwapExactIn_sDaiToUsdc() public view { + assertEq(psm.previewSwapExactIn(address(sDai), address(usdc), 1e18), 1.25e6); + assertEq(psm.previewSwapExactIn(address(sDai), address(usdc), 2e18), 2.5e6); + assertEq(psm.previewSwapExactIn(address(sDai), address(usdc), 3e18), 3.75e6); } - function testFuzz_previewSwap_sDaiToUsdc(uint256 amountIn, uint256 conversionRate) public { + function testFuzz_previewSwapExactIn_sDaiToUsdc(uint256 amountIn, uint256 conversionRate) public { amountIn = _bound(amountIn, 1, SDAI_TOKEN_MAX); conversionRate = _bound(conversionRate, 0.0001e27, 1000e27); // 0.01% to 100,000% conversion rate @@ -136,8 +231,45 @@ contract PSMPreviewSwapSDaiAssetInTests is PSMTestBase { uint256 amountOut = amountIn * conversionRate / 1e27 / 1e12; - assertEq(psm.previewSwap(address(sDai), address(usdc), amountIn), amountOut); + assertEq(psm.previewSwapExactIn(address(sDai), address(usdc), amountIn), amountOut); } } +contract PSMPreviewSwapExactOut_SDaiAssetInTests is PSMTestBase { + + function test_previewSwapExactOut_sDaiToDai() public view { + assertEq(psm.previewSwapExactOut(address(sDai), address(dai), 1.25e18), 1e18); + assertEq(psm.previewSwapExactOut(address(sDai), address(dai), 2.5e18), 2e18); + assertEq(psm.previewSwapExactOut(address(sDai), address(dai), 3.75e18), 3e18); + } + + function testFuzz_previewSwapExactOut_sDaiToDai(uint256 amountOut, uint256 conversionRate) public { + amountOut = _bound(amountOut, 1, DAI_TOKEN_MAX); + conversionRate = _bound(conversionRate, 0.0001e27, 1000e27); // 0.01% to 100,000% conversion rate + + rateProvider.__setConversionRate(conversionRate); + + uint256 amountIn = amountOut * 1e27 / conversionRate; + + assertEq(psm.previewSwapExactOut(address(sDai), address(dai), amountOut), amountIn); + } + + function test_previewSwapExactOut_sDaiToUsdc() public view { + assertEq(psm.previewSwapExactOut(address(sDai), address(usdc), 1.25e6), 1e18); + assertEq(psm.previewSwapExactOut(address(sDai), address(usdc), 2.5e6), 2e18); + assertEq(psm.previewSwapExactOut(address(sDai), address(usdc), 3.75e6), 3e18); + } + + function testFuzz_previewSwapExactOut_sDaiToUsdc(uint256 amountOut, uint256 conversionRate) public { + amountOut = bound(amountOut, 1, USDC_TOKEN_MAX); + conversionRate = bound(conversionRate, 0.0001e27, 1000e27); // 0.01% to 100,000% conversion rate + + rateProvider.__setConversionRate(conversionRate); + + uint256 amountIn = amountOut * 1e27 / conversionRate * 1e12; + + assertEq(psm.previewSwapExactOut(address(sDai), address(usdc), amountOut), amountIn); + } + +} diff --git a/test/unit/Swaps.t.sol b/test/unit/Swaps.t.sol index 0e779a7..3d1f56f 100644 --- a/test/unit/Swaps.t.sol +++ b/test/unit/Swaps.t.sol @@ -62,7 +62,7 @@ contract PSMSwapFailureTests is PSMTestBase { usdc.approve(address(psm), 100e6); - uint256 expectedAmountOut = psm.previewSwap(address(usdc), address(sDai), 100e6); + uint256 expectedAmountOut = psm.previewSwapExactIn(address(usdc), address(sDai), 100e6); assertEq(expectedAmountOut, 80e18); @@ -112,7 +112,7 @@ contract PSMSwapFailureTests is PSMTestBase { usdc.approve(address(psm), 125e6 + 2); - uint256 expectedAmountOut = psm.previewSwap(address(usdc), address(sDai), 125e6 + 2); + uint256 expectedAmountOut = psm.previewSwapExactIn(address(usdc), address(sDai), 125e6 + 2); assertEq(expectedAmountOut, 100.000001e18); // More than balance of sDAI From 1c42d9979f132514e977d3a1389103ff635d25c0 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Wed, 3 Jul 2024 06:45:25 -0400 Subject: [PATCH 03/21] feat: add new function --- src/PSM3.sol | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/PSM3.sol b/src/PSM3.sol index 8d49b50..3f109b1 100644 --- a/src/PSM3.sol +++ b/src/PSM3.sol @@ -79,6 +79,29 @@ contract PSM3 is IPSM3 { emit Swap(assetIn, assetOut, msg.sender, receiver, amountIn, amountOut, referralCode); } + function swapExactOut( + address assetIn, + address assetOut, + uint256 amountOut, + uint256 minAmountIn, + address receiver, + uint256 referralCode + ) + external //override + { + require(amountOut != 0, "PSM3/invalid-amountOut"); + require(receiver != address(0), "PSM3/invalid-receiver"); + + uint256 amountIn = previewSwapExactOut(assetIn, assetOut, amountOut); + + require(amountIn >= minAmountIn, "PSM3/amountIn-too-low"); + + IERC20(assetIn).safeTransferFrom(msg.sender, address(this), amountIn); + IERC20(assetOut).safeTransfer(receiver, amountOut); + + emit Swap(assetIn, assetOut, msg.sender, receiver, amountIn, amountOut, referralCode); + } + /**********************************************************************************************/ /*** Liquidity provision functions ***/ /**********************************************************************************************/ From 278d99eda02c15d57e98ac7116f9171d9ef0bfd2 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Wed, 3 Jul 2024 21:41:38 -0400 Subject: [PATCH 04/21] feat: create new file, failure tests passing --- src/PSM3.sol | 6 +- src/interfaces/IPSM3.sol | 21 ++ test/unit/{Swaps.t.sol => SwapExactIn.t.sol} | 106 +++--- test/unit/SwapExactOut.t.sol | 341 +++++++++++++++++++ 4 files changed, 418 insertions(+), 56 deletions(-) rename test/unit/{Swaps.t.sol => SwapExactIn.t.sol} (68%) create mode 100644 test/unit/SwapExactOut.t.sol diff --git a/src/PSM3.sol b/src/PSM3.sol index 3f109b1..81162ad 100644 --- a/src/PSM3.sol +++ b/src/PSM3.sol @@ -83,18 +83,18 @@ contract PSM3 is IPSM3 { address assetIn, address assetOut, uint256 amountOut, - uint256 minAmountIn, + uint256 maxAmountIn, address receiver, uint256 referralCode ) - external //override + external override { require(amountOut != 0, "PSM3/invalid-amountOut"); require(receiver != address(0), "PSM3/invalid-receiver"); uint256 amountIn = previewSwapExactOut(assetIn, assetOut, amountOut); - require(amountIn >= minAmountIn, "PSM3/amountIn-too-low"); + require(amountIn <= maxAmountIn, "PSM3/amountIn-too-high"); IERC20(assetIn).safeTransferFrom(msg.sender, address(this), amountIn); IERC20(assetOut).safeTransfer(receiver, amountOut); diff --git a/src/interfaces/IPSM3.sol b/src/interfaces/IPSM3.sol index 45a1f64..a22a31a 100644 --- a/src/interfaces/IPSM3.sol +++ b/src/interfaces/IPSM3.sol @@ -133,6 +133,27 @@ interface IPSM3 { uint256 referralCode ) external; + /** + * @dev Swaps a derived amount of assetIn for a specific amount of assetOut in the PSM. The + * amount swapped is converted based on the current value of the two assets used in + * the swap. This function will revert if there is not enough balance in the PSM to + * facilitate the swap. Both assets must be supported in the PSM in order to succeed. + * @param assetIn Address of the ERC-20 asset to swap in. + * @param assetOut Address of the ERC-20 asset to swap out. + * @param amountIn Amount of the asset to swap in. + * @param minAmountOut Minimum amount of the asset to receive. + * @param receiver Address of the receiver of the swapped assets. + * @param referralCode Referral code for the swap. + */ + function swapExactOut( + address assetIn, + address assetOut, + uint256 amountIn, + uint256 minAmountOut, + address receiver, + uint256 referralCode + ) external; + /**********************************************************************************************/ /*** Liquidity provision functions ***/ /**********************************************************************************************/ diff --git a/test/unit/Swaps.t.sol b/test/unit/SwapExactIn.t.sol similarity index 68% rename from test/unit/Swaps.t.sol rename to test/unit/SwapExactIn.t.sol index 3d1f56f..92389e0 100644 --- a/test/unit/Swaps.t.sol +++ b/test/unit/SwapExactIn.t.sol @@ -7,7 +7,7 @@ import { PSM3 } from "src/PSM3.sol"; import { MockERC20, PSMTestBase } from "test/PSMTestBase.sol"; -contract PSMSwapFailureTests is PSMTestBase { +contract PSMSwapExactInFailureTests is PSMTestBase { address public swapper = makeAddr("swapper"); address public receiver = makeAddr("receiver"); @@ -20,42 +20,42 @@ contract PSMSwapFailureTests is PSMTestBase { sDai.mint(address(psm), 100e18); } - function test_swap_amountZero() public { + function test_swapExactIn_amountZero() public { vm.expectRevert("PSM3/invalid-amountIn"); psm.swapExactIn(address(usdc), address(sDai), 0, 0, receiver, 0); } - function test_swap_receiverZero() public { + function test_swapExactIn_receiverZero() public { vm.expectRevert("PSM3/invalid-receiver"); psm.swapExactIn(address(usdc), address(sDai), 100e6, 80e18, address(0), 0); } - function test_swap_invalid_assetIn() public { + function test_swapExactIn_invalid_assetIn() public { vm.expectRevert("PSM3/invalid-asset"); psm.swapExactIn(makeAddr("other-token"), address(sDai), 100e6, 80e18, receiver, 0); } - function test_swap_invalid_assetOut() public { + function test_swapExactIn_invalid_assetOut() public { vm.expectRevert("PSM3/invalid-asset"); psm.swapExactIn(address(usdc), makeAddr("other-token"), 100e6, 80e18, receiver, 0); } - function test_swap_bothAsset0() public { + function test_swapExactIn_bothAsset0() public { vm.expectRevert("PSM3/invalid-asset"); psm.swapExactIn(address(dai), address(dai), 100e6, 80e18, receiver, 0); } - function test_swap_bothAsset1() public { + function test_swapExactIn_bothAsset1() public { vm.expectRevert("PSM3/invalid-asset"); psm.swapExactIn(address(usdc), address(usdc), 100e6, 80e18, receiver, 0); } - function test_swap_bothAsset2() public { + function test_swapExactIn_bothAsset2() public { vm.expectRevert("PSM3/invalid-asset"); psm.swapExactIn(address(sDai), address(sDai), 100e6, 80e18, receiver, 0); } - function test_swap_minAmountOutBoundary() public { + function test_swapExactIn_minAmountOutBoundary() public { usdc.mint(swapper, 100e6); vm.startPrank(swapper); @@ -72,7 +72,7 @@ contract PSMSwapFailureTests is PSMTestBase { psm.swapExactIn(address(usdc), address(sDai), 100e6, 80e18, receiver, 0); } - function test_swap_insufficientApproveBoundary() public { + function test_swapExactIn_insufficientApproveBoundary() public { usdc.mint(swapper, 100e6); vm.startPrank(swapper); @@ -87,7 +87,7 @@ contract PSMSwapFailureTests is PSMTestBase { psm.swapExactIn(address(usdc), address(sDai), 100e6, 80e18, receiver, 0); } - function test_swap_insufficientUserBalanceBoundary() public { + function test_swapExactIn_insufficientUserBalanceBoundary() public { usdc.mint(swapper, 100e6 - 1); vm.startPrank(swapper); @@ -102,7 +102,7 @@ contract PSMSwapFailureTests is PSMTestBase { psm.swapExactIn(address(usdc), address(sDai), 100e6, 80e18, receiver, 0); } - function test_swap_insufficientPsmBalanceBoundary() public { + function test_swapExactIn_insufficientPsmBalanceBoundary() public { // NOTE: Using 2 instead of 1 here because 1/1.25 rounds to 0, 2/1.25 rounds to 1 // this is because the conversion rate is divided out before the precision conversion // is done. @@ -124,7 +124,7 @@ contract PSMSwapFailureTests is PSMTestBase { } -contract PSMSwapSuccessTestsBase is PSMTestBase { +contract PSMSwapExactInSuccessTestsBase is PSMTestBase { address public swapper = makeAddr("swapper"); address public receiver = makeAddr("receiver"); @@ -139,7 +139,7 @@ contract PSMSwapSuccessTestsBase is PSMTestBase { sDai.mint(address(psm), SDAI_TOKEN_MAX * 100); } - function _swapTest( + function _swapExactInTest( MockERC20 assetIn, MockERC20 assetOut, uint256 amountIn, @@ -178,25 +178,25 @@ contract PSMSwapSuccessTestsBase is PSMTestBase { } -contract PSMSwapDaiAssetInTests is PSMSwapSuccessTestsBase { +contract PSMSwapExactInDaiAssetInTests is PSMSwapExactInSuccessTestsBase { - function test_swap_daiToUsdc_sameReceiver() public assertAtomicPsmValueDoesNotChange { - _swapTest(dai, usdc, 100e18, 100e6, swapper, swapper); + function test_swapExactIn_daiToUsdc_sameReceiver() public assertAtomicPsmValueDoesNotChange { + _swapExactInTest(dai, usdc, 100e18, 100e6, swapper, swapper); } - function test_swap_daiToSDai_sameReceiver() public assertAtomicPsmValueDoesNotChange { - _swapTest(dai, sDai, 100e18, 80e18, swapper, swapper); + function test_swapExactIn_daiToSDai_sameReceiver() public assertAtomicPsmValueDoesNotChange { + _swapExactInTest(dai, sDai, 100e18, 80e18, swapper, swapper); } - function test_swap_daiToUsdc_differentReceiver() public assertAtomicPsmValueDoesNotChange { - _swapTest(dai, usdc, 100e18, 100e6, swapper, receiver); + function test_swapExactIn_daiToUsdc_differentReceiver() public assertAtomicPsmValueDoesNotChange { + _swapExactInTest(dai, usdc, 100e18, 100e6, swapper, receiver); } - function test_swap_daiToSDai_differentReceiver() public assertAtomicPsmValueDoesNotChange { - _swapTest(dai, sDai, 100e18, 80e18, swapper, receiver); + function test_swapExactIn_daiToSDai_differentReceiver() public assertAtomicPsmValueDoesNotChange { + _swapExactInTest(dai, sDai, 100e18, 80e18, swapper, receiver); } - function testFuzz_swap_daiToUsdc( + function testFuzz_swapExactIn_daiToUsdc( uint256 amountIn, address fuzzSwapper, address fuzzReceiver @@ -207,10 +207,10 @@ contract PSMSwapDaiAssetInTests is PSMSwapSuccessTestsBase { amountIn = _bound(amountIn, 1, DAI_TOKEN_MAX); // Zero amount reverts uint256 amountOut = amountIn / 1e12; - _swapTest(dai, usdc, amountIn, amountOut, fuzzSwapper, fuzzReceiver); + _swapExactInTest(dai, usdc, amountIn, amountOut, fuzzSwapper, fuzzReceiver); } - function testFuzz_swap_daiToSDai( + function testFuzz_swapExactIn_daiToSDai( uint256 amountIn, uint256 conversionRate, address fuzzSwapper, @@ -226,30 +226,30 @@ contract PSMSwapDaiAssetInTests is PSMSwapSuccessTestsBase { uint256 amountOut = amountIn * 1e27 / conversionRate; - _swapTest(dai, sDai, amountIn, amountOut, fuzzSwapper, fuzzReceiver); + _swapExactInTest(dai, sDai, amountIn, amountOut, fuzzSwapper, fuzzReceiver); } } -contract PSMSwapUsdcAssetInTests is PSMSwapSuccessTestsBase { +contract PSMSwapExactInUsdcAssetInTests is PSMSwapExactInSuccessTestsBase { - function test_swap_usdcToDai_sameReceiver() public assertAtomicPsmValueDoesNotChange { - _swapTest(usdc, dai, 100e6, 100e18, swapper, swapper); + function test_swapExactIn_usdcToDai_sameReceiver() public assertAtomicPsmValueDoesNotChange { + _swapExactInTest(usdc, dai, 100e6, 100e18, swapper, swapper); } - function test_swap_usdcToSDai_sameReceiver() public assertAtomicPsmValueDoesNotChange { - _swapTest(usdc, sDai, 100e6, 80e18, swapper, swapper); + function test_swapExactIn_usdcToSDai_sameReceiver() public assertAtomicPsmValueDoesNotChange { + _swapExactInTest(usdc, sDai, 100e6, 80e18, swapper, swapper); } - function test_swap_usdcToDai_differentReceiver() public assertAtomicPsmValueDoesNotChange { - _swapTest(usdc, dai, 100e6, 100e18, swapper, receiver); + function test_swapExactIn_usdcToDai_differentReceiver() public assertAtomicPsmValueDoesNotChange { + _swapExactInTest(usdc, dai, 100e6, 100e18, swapper, receiver); } - function test_swap_usdcToSDai_differentReceiver() public assertAtomicPsmValueDoesNotChange { - _swapTest(usdc, sDai, 100e6, 80e18, swapper, receiver); + function test_swapExactIn_usdcToSDai_differentReceiver() public assertAtomicPsmValueDoesNotChange { + _swapExactInTest(usdc, sDai, 100e6, 80e18, swapper, receiver); } - function testFuzz_swap_usdcToDai( + function testFuzz_swapExactIn_usdcToDai( uint256 amountIn, address fuzzSwapper, address fuzzReceiver @@ -260,10 +260,10 @@ contract PSMSwapUsdcAssetInTests is PSMSwapSuccessTestsBase { amountIn = _bound(amountIn, 1, USDC_TOKEN_MAX); // Zero amount reverts uint256 amountOut = amountIn * 1e12; - _swapTest(usdc, dai, amountIn, amountOut, fuzzSwapper, fuzzReceiver); + _swapExactInTest(usdc, dai, amountIn, amountOut, fuzzSwapper, fuzzReceiver); } - function testFuzz_swap_usdcToSDai( + function testFuzz_swapExactIn_usdcToSDai( uint256 amountIn, uint256 conversionRate, address fuzzSwapper, @@ -280,30 +280,30 @@ contract PSMSwapUsdcAssetInTests is PSMSwapSuccessTestsBase { uint256 amountOut = amountIn * 1e27 / conversionRate * 1e12; - _swapTest(usdc, sDai, amountIn, amountOut, fuzzSwapper, fuzzReceiver); + _swapExactInTest(usdc, sDai, amountIn, amountOut, fuzzSwapper, fuzzReceiver); } } -contract PSMSwapSDaiAssetInTests is PSMSwapSuccessTestsBase { +contract PSMSwapExactInSDaiAssetInTests is PSMSwapExactInSuccessTestsBase { - function test_swap_sDaiToDai_sameReceiver() public assertAtomicPsmValueDoesNotChange { - _swapTest(sDai, dai, 100e18, 125e18, swapper, swapper); + function test_swapExactIn_sDaiToDai_sameReceiver() public assertAtomicPsmValueDoesNotChange { + _swapExactInTest(sDai, dai, 100e18, 125e18, swapper, swapper); } - function test_swap_sDaiToUsdc_sameReceiver() public assertAtomicPsmValueDoesNotChange { - _swapTest(sDai, usdc, 100e18, 125e6, swapper, swapper); + function test_swapExactIn_sDaiToUsdc_sameReceiver() public assertAtomicPsmValueDoesNotChange { + _swapExactInTest(sDai, usdc, 100e18, 125e6, swapper, swapper); } - function test_swap_sDaiToDai_differentReceiver() public assertAtomicPsmValueDoesNotChange { - _swapTest(sDai, dai, 100e18, 125e18, swapper, receiver); + function test_swapExactIn_sDaiToDai_differentReceiver() public assertAtomicPsmValueDoesNotChange { + _swapExactInTest(sDai, dai, 100e18, 125e18, swapper, receiver); } - function test_swap_sDaiToUsdc_differentReceiver() public assertAtomicPsmValueDoesNotChange { - _swapTest(sDai, usdc, 100e18, 125e6, swapper, receiver); + function test_swapExactIn_sDaiToUsdc_differentReceiver() public assertAtomicPsmValueDoesNotChange { + _swapExactInTest(sDai, usdc, 100e18, 125e6, swapper, receiver); } - function testFuzz_swap_sDaiToDai( + function testFuzz_swapExactIn_sDaiToDai( uint256 amountIn, uint256 conversionRate, address fuzzSwapper, @@ -320,10 +320,10 @@ contract PSMSwapSDaiAssetInTests is PSMSwapSuccessTestsBase { uint256 amountOut = amountIn * conversionRate / 1e27; - _swapTest(sDai, dai, amountIn, amountOut, fuzzSwapper, fuzzReceiver); + _swapExactInTest(sDai, dai, amountIn, amountOut, fuzzSwapper, fuzzReceiver); } - function testFuzz_swap_sDaiToUsdc( + function testFuzz_swapExactIn_sDaiToUsdc( uint256 amountIn, uint256 conversionRate, address fuzzSwapper, @@ -340,7 +340,7 @@ contract PSMSwapSDaiAssetInTests is PSMSwapSuccessTestsBase { uint256 amountOut = amountIn * conversionRate / 1e27 / 1e12; - _swapTest(sDai, usdc, amountIn, amountOut, fuzzSwapper, fuzzReceiver); + _swapExactInTest(sDai, usdc, amountIn, amountOut, fuzzSwapper, fuzzReceiver); } } diff --git a/test/unit/SwapExactOut.t.sol b/test/unit/SwapExactOut.t.sol new file mode 100644 index 0000000..6b3693d --- /dev/null +++ b/test/unit/SwapExactOut.t.sol @@ -0,0 +1,341 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; + +import { PSM3 } from "src/PSM3.sol"; + +import { MockERC20, PSMTestBase } from "test/PSMTestBase.sol"; + +contract PSMSwapExactOutFailureTests is PSMTestBase { + + address public swapper = makeAddr("swapper"); + address public receiver = makeAddr("receiver"); + + function setUp() public override { + super.setUp(); + + // Needed for boundary success conditions + usdc.mint(address(psm), 100e6); + sDai.mint(address(psm), 100e18); + } + + function test_swapExactOut_amountZero() public { + vm.expectRevert("PSM3/invalid-amountOut"); + psm.swapExactOut(address(usdc), address(sDai), 0, 0, receiver, 0); + } + + function test_swapExactOut_receiverZero() public { + vm.expectRevert("PSM3/invalid-receiver"); + psm.swapExactOut(address(usdc), address(sDai), 100e6, 80e18, address(0), 0); + } + + function test_swapExactOut_invalid_assetIn() public { + vm.expectRevert("PSM3/invalid-asset"); + psm.swapExactOut(makeAddr("other-token"), address(sDai), 100e6, 80e18, receiver, 0); + } + + function test_swapExactOut_invalid_assetOut() public { + vm.expectRevert("PSM3/invalid-asset"); + psm.swapExactOut(address(usdc), makeAddr("other-token"), 100e6, 80e18, receiver, 0); + } + + function test_swapExactOut_bothAsset0() public { + vm.expectRevert("PSM3/invalid-asset"); + psm.swapExactOut(address(dai), address(dai), 100e6, 80e18, receiver, 0); + } + + function test_swapExactOut_bothAsset1() public { + vm.expectRevert("PSM3/invalid-asset"); + psm.swapExactOut(address(usdc), address(usdc), 100e6, 80e18, receiver, 0); + } + + function test_swapExactOut_bothAsset2() public { + vm.expectRevert("PSM3/invalid-asset"); + psm.swapExactOut(address(sDai), address(sDai), 100e6, 80e18, receiver, 0); + } + + function test_swapExactOut_maxAmountBoundary() public { + usdc.mint(swapper, 100e6); + + vm.startPrank(swapper); + + usdc.approve(address(psm), 100e6); + + uint256 expectedAmountIn = psm.previewSwapExactOut(address(usdc), address(sDai), 80e18); + + assertEq(expectedAmountIn, 100e6); + + vm.expectRevert("PSM3/amountIn-too-high"); + psm.swapExactOut(address(usdc), address(sDai), 80e18, 100e6 - 1, receiver, 0); + + psm.swapExactOut(address(usdc), address(sDai), 80e18, 100e6, receiver, 0); + } + + function test_swapExactOut_insufficientApproveBoundary() public { + usdc.mint(swapper, 100e6); + + vm.startPrank(swapper); + + usdc.approve(address(psm), 100e6 - 1); + + vm.expectRevert("SafeERC20/transfer-from-failed"); + psm.swapExactOut(address(usdc), address(sDai), 80e18, 100e6, receiver, 0); + + usdc.approve(address(psm), 100e6); + + psm.swapExactOut(address(usdc), address(sDai), 80e18, 100e6, receiver, 0); + } + + function test_swapExactOut_insufficientUserBalanceBoundary() public { + usdc.mint(swapper, 100e6 - 1); + + vm.startPrank(swapper); + + usdc.approve(address(psm), 100e6); + + vm.expectRevert("SafeERC20/transfer-from-failed"); + psm.swapExactOut(address(usdc), address(sDai), 80e18, 100e6, receiver, 0); + + usdc.mint(swapper, 1); + + psm.swapExactOut(address(usdc), address(sDai), 80e18, 100e6, receiver, 0); + } + + function test_swapExactOut_insufficientPsmBalanceBoundary() public { + // NOTE: Users can get up to 1e12 more sDAI from the swap because of rounding + // TODO: Figure out how to make this round down always + usdc.mint(swapper, 125e6); + + vm.startPrank(swapper); + + usdc.approve(address(psm), 125e6); + + vm.expectRevert("SafeERC20/transfer-failed"); + psm.swapExactOut(address(usdc), address(sDai), 100e18 + 1, 125e6, receiver, 0); + + psm.swapExactOut(address(usdc), address(sDai), 100e18, 125e6, receiver, 0); + } + +} + +contract PSMSwapExactOutSuccessTestsBase is PSMTestBase { + + address public swapper = makeAddr("swapper"); + address public receiver = makeAddr("receiver"); + + function setUp() public override { + super.setUp(); + + // Mint 100x higher than max amount for each token (max conversion rate) + // Covers both lower and upper bounds of conversion rate (1% to 10,000% are both 100x) + dai.mint(address(psm), DAI_TOKEN_MAX * 100); + usdc.mint(address(psm), USDC_TOKEN_MAX * 100); + sDai.mint(address(psm), SDAI_TOKEN_MAX * 100); + } + + function _swapExactOutTest( + MockERC20 assetIn, + MockERC20 assetOut, + uint256 amountIn, + uint256 amountOut, + address swapper_, + address receiver_ + ) internal { + // 100 trillion of each token corresponds to original mint amount + uint256 psmAssetInBalance = 100_000_000_000_000 * 10 ** assetIn.decimals(); + uint256 psmAssetOutBalance = 100_000_000_000_000 * 10 ** assetOut.decimals(); + + assetIn.mint(swapper_, amountIn); + + vm.startPrank(swapper_); + + assetIn.approve(address(psm), amountIn); + + assertEq(assetIn.allowance(swapper_, address(psm)), amountIn); + + assertEq(assetIn.balanceOf(swapper_), amountIn); + assertEq(assetIn.balanceOf(address(psm)), psmAssetInBalance); + + assertEq(assetOut.balanceOf(receiver_), 0); + assertEq(assetOut.balanceOf(address(psm)), psmAssetOutBalance); + + psm.swapExactOut(address(assetIn), address(assetOut), amountIn, amountOut, receiver_, 0); + + assertEq(assetIn.allowance(swapper_, address(psm)), 0); + + assertEq(assetIn.balanceOf(swapper_), 0); + assertEq(assetIn.balanceOf(address(psm)), psmAssetInBalance + amountIn); + + assertEq(assetOut.balanceOf(receiver_), amountOut); + assertEq(assetOut.balanceOf(address(psm)), psmAssetOutBalance - amountOut); + } + +} + +contract PSMSwapExactOutDaiAssetInTests is PSMSwapExactOutSuccessTestsBase { + + function test_swapExactOut_daiToUsdc_sameReceiver() public assertAtomicPsmValueDoesNotChange { + _swapExactOutTest(dai, usdc, 100e18, 100e6, swapper, swapper); + } + + function test_swapExactOut_daiToSDai_sameReceiver() public assertAtomicPsmValueDoesNotChange { + _swapExactOutTest(dai, sDai, 100e18, 80e18, swapper, swapper); + } + + function test_swapExactOut_daiToUsdc_differentReceiver() public assertAtomicPsmValueDoesNotChange { + _swapExactOutTest(dai, usdc, 100e18, 100e6, swapper, receiver); + } + + function test_swapExactOut_daiToSDai_differentReceiver() public assertAtomicPsmValueDoesNotChange { + _swapExactOutTest(dai, sDai, 100e18, 80e18, swapper, receiver); + } + + function testFuzz_swapExactOut_daiToUsdc( + uint256 amountIn, + address fuzzSwapper, + address fuzzReceiver + ) public { + vm.assume(fuzzSwapper != address(psm)); + vm.assume(fuzzReceiver != address(psm)); + vm.assume(fuzzReceiver != address(0)); + + amountIn = _bound(amountIn, 1, DAI_TOKEN_MAX); // Zero amount reverts + uint256 amountOut = amountIn / 1e12; + _swapExactOutTest(dai, usdc, amountIn, amountOut, fuzzSwapper, fuzzReceiver); + } + + function testFuzz_swapExactOut_daiToSDai( + uint256 amountIn, + uint256 conversionRate, + address fuzzSwapper, + address fuzzReceiver + ) public { + vm.assume(fuzzSwapper != address(psm)); + vm.assume(fuzzReceiver != address(psm)); + vm.assume(fuzzReceiver != address(0)); + + amountIn = _bound(amountIn, 1, DAI_TOKEN_MAX); + conversionRate = _bound(conversionRate, 0.01e27, 100e27); // 1% to 10,000% conversion rate + rateProvider.__setConversionRate(conversionRate); + + uint256 amountOut = amountIn * 1e27 / conversionRate; + + _swapExactOutTest(dai, sDai, amountIn, amountOut, fuzzSwapper, fuzzReceiver); + } + +} + +contract PSMSwapExactOutUsdcAssetInTests is PSMSwapExactOutSuccessTestsBase { + + function test_swapExactOut_usdcToDai_sameReceiver() public assertAtomicPsmValueDoesNotChange { + _swapExactOutTest(usdc, dai, 100e6, 100e18, swapper, swapper); + } + + function test_swapExactOut_usdcToSDai_sameReceiver() public assertAtomicPsmValueDoesNotChange { + _swapExactOutTest(usdc, sDai, 100e6, 80e18, swapper, swapper); + } + + function test_swapExactOut_usdcToDai_differentReceiver() public assertAtomicPsmValueDoesNotChange { + _swapExactOutTest(usdc, dai, 100e6, 100e18, swapper, receiver); + } + + function test_swapExactOut_usdcToSDai_differentReceiver() public assertAtomicPsmValueDoesNotChange { + _swapExactOutTest(usdc, sDai, 100e6, 80e18, swapper, receiver); + } + + function testFuzz_swapExactOut_usdcToDai( + uint256 amountIn, + address fuzzSwapper, + address fuzzReceiver + ) public { + vm.assume(fuzzSwapper != address(psm)); + vm.assume(fuzzReceiver != address(psm)); + vm.assume(fuzzReceiver != address(0)); + + amountIn = _bound(amountIn, 1, USDC_TOKEN_MAX); // Zero amount reverts + uint256 amountOut = amountIn * 1e12; + _swapExactOutTest(usdc, dai, amountIn, amountOut, fuzzSwapper, fuzzReceiver); + } + + function testFuzz_swapExactOut_usdcToSDai( + uint256 amountIn, + uint256 conversionRate, + address fuzzSwapper, + address fuzzReceiver + ) public { + vm.assume(fuzzSwapper != address(psm)); + vm.assume(fuzzReceiver != address(psm)); + vm.assume(fuzzReceiver != address(0)); + + amountIn = _bound(amountIn, 1, USDC_TOKEN_MAX); + conversionRate = _bound(conversionRate, 0.01e27, 100e27); // 1% to 10,000% conversion rate + + rateProvider.__setConversionRate(conversionRate); + + uint256 amountOut = amountIn * 1e27 / conversionRate * 1e12; + + _swapExactOutTest(usdc, sDai, amountIn, amountOut, fuzzSwapper, fuzzReceiver); + } + +} + +contract PSMSwapExactOutSDaiAssetInTests is PSMSwapExactOutSuccessTestsBase { + + function test_swapExactOut_sDaiToDai_sameReceiver() public assertAtomicPsmValueDoesNotChange { + _swapExactOutTest(sDai, dai, 100e18, 125e18, swapper, swapper); + } + + function test_swapExactOut_sDaiToUsdc_sameReceiver() public assertAtomicPsmValueDoesNotChange { + _swapExactOutTest(sDai, usdc, 100e18, 125e6, swapper, swapper); + } + + function test_swapExactOut_sDaiToDai_differentReceiver() public assertAtomicPsmValueDoesNotChange { + _swapExactOutTest(sDai, dai, 100e18, 125e18, swapper, receiver); + } + + function test_swapExactOut_sDaiToUsdc_differentReceiver() public assertAtomicPsmValueDoesNotChange { + _swapExactOutTest(sDai, usdc, 100e18, 125e6, swapper, receiver); + } + + function testFuzz_swapExactOut_sDaiToDai( + uint256 amountIn, + uint256 conversionRate, + address fuzzSwapper, + address fuzzReceiver + ) public { + vm.assume(fuzzSwapper != address(psm)); + vm.assume(fuzzReceiver != address(psm)); + vm.assume(fuzzReceiver != address(0)); + + amountIn = _bound(amountIn, 1, SDAI_TOKEN_MAX); + conversionRate = _bound(conversionRate, 0.01e27, 100e27); // 1% to 10,000% conversion rate + + rateProvider.__setConversionRate(conversionRate); + + uint256 amountOut = amountIn * conversionRate / 1e27; + + _swapExactOutTest(sDai, dai, amountIn, amountOut, fuzzSwapper, fuzzReceiver); + } + + function testFuzz_swapExactOut_sDaiToUsdc( + uint256 amountIn, + uint256 conversionRate, + address fuzzSwapper, + address fuzzReceiver + ) public { + vm.assume(fuzzSwapper != address(psm)); + vm.assume(fuzzReceiver != address(psm)); + vm.assume(fuzzReceiver != address(0)); + + amountIn = _bound(amountIn, 1, SDAI_TOKEN_MAX); + conversionRate = _bound(conversionRate, 0.01e27, 100e27); // 1% to 10,000% conversion rate + + rateProvider.__setConversionRate(conversionRate); + + uint256 amountOut = amountIn * conversionRate / 1e27 / 1e12; + + _swapExactOutTest(sDai, usdc, amountIn, amountOut, fuzzSwapper, fuzzReceiver); + } + +} From 54ca402411bfd694c55dc0dddca2a27e60ac67c9 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Thu, 4 Jul 2024 06:57:07 -0400 Subject: [PATCH 05/21] ci: add coverage backgp --- .github/workflows/pr.yml | 86 ++++++++++++++++----------------- test/unit/Withdraw.t.sol | 100 +++++++++++++++++++++------------------ 2 files changed, 98 insertions(+), 88 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 5614607..cd1a1fa 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -37,51 +37,51 @@ jobs: BASE_RPC_URL: ${{secrets.BASE_RPC_URL}} run: FOUNDRY_PROFILE=pr forge test -vv --show-progress - # coverage: - # runs-on: ubuntu-latest - # steps: - # - uses: actions/checkout@v3 + coverage: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 - # - name: Install Foundry - # uses: foundry-rs/foundry-toolchain@v1 + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 - # - name: Run coverage - # env: - # MAINNET_RPC_URL: ${{secrets.MAINNET_RPC_URL}} - # OPTIMISM_RPC_URL: ${{secrets.OPTIMISM_RPC_URL}} - # ARBITRUM_ONE_RPC_URL: ${{secrets.ARBITRUM_ONE_RPC_URL}} - # ARBITRUM_NOVA_RPC_URL: ${{secrets.ARBITRUM_NOVA_RPC_URL}} - # GNOSIS_CHAIN_RPC_URL: ${{secrets.GNOSIS_CHAIN_RPC_URL}} - # BASE_RPC_URL: ${{secrets.BASE_RPC_URL}} - # run: forge coverage --report summary --report lcov + - name: Run coverage + env: + MAINNET_RPC_URL: ${{secrets.MAINNET_RPC_URL}} + OPTIMISM_RPC_URL: ${{secrets.OPTIMISM_RPC_URL}} + ARBITRUM_ONE_RPC_URL: ${{secrets.ARBITRUM_ONE_RPC_URL}} + ARBITRUM_NOVA_RPC_URL: ${{secrets.ARBITRUM_NOVA_RPC_URL}} + GNOSIS_CHAIN_RPC_URL: ${{secrets.GNOSIS_CHAIN_RPC_URL}} + BASE_RPC_URL: ${{secrets.BASE_RPC_URL}} + run: forge coverage --report summary --report lcov - # # To ignore coverage for certain directories modify the paths in this step as needed. The - # # below default ignores coverage results for the test and script directories. Alternatively, - # # to include coverage in all directories, comment out this step. Note that because this - # # filtering applies to the lcov file, the summary table generated in the previous step will - # # still include all files and directories. - # # The `--rc lcov_branch_coverage=1` part keeps branch info in the filtered report, since lcov - # # defaults to removing branch info. - # - name: Filter directories - # run: | - # sudo apt update && sudo apt install -y lcov - # lcov --remove lcov.info 'test/*' 'script/*' --output-file lcov.info --rc lcov_branch_coverage=1 + # To ignore coverage for certain directories modify the paths in this step as needed. The + # below default ignores coverage results for the test and script directories. Alternatively, + # to include coverage in all directories, comment out this step. Note that because this + # filtering applies to the lcov file, the summary table generated in the previous step will + # still include all files and directories. + # The `--rc lcov_branch_coverage=1` part keeps branch info in the filtered report, since lcov + # defaults to removing branch info. + - name: Filter directories + run: | + sudo apt update && sudo apt install -y lcov + lcov --remove lcov.info 'test/*' 'script/*' --output-file lcov.info --rc lcov_branch_coverage=1 - # # This step posts a detailed coverage report as a comment and deletes previous comments on - # # each push. The below step is used to fail coverage if the specified coverage threshold is - # # not met. The below step can post a comment (when it's `github-token` is specified) but it's - # # not as useful, and this action cannot fail CI based on a minimum coverage threshold, which - # # is why we use both in this way. - # - name: Post coverage report - # if: github.event_name == 'pull_request' # This action fails when ran outside of a pull request. - # uses: romeovs/lcov-reporter-action@v0.3.1 - # with: - # delete-old-comments: true - # lcov-file: ./lcov.info - # github-token: ${{ secrets.GITHUB_TOKEN }} # Adds a coverage summary comment to the PR. + # This step posts a detailed coverage report as a comment and deletes previous comments on + # each push. The below step is used to fail coverage if the specified coverage threshold is + # not met. The below step can post a comment (when it's `github-token` is specified) but it's + # not as useful, and this action cannot fail CI based on a minimum coverage threshold, which + # is why we use both in this way. + - name: Post coverage report + if: github.event_name == 'pull_request' # This action fails when ran outside of a pull request. + uses: romeovs/lcov-reporter-action@v0.3.1 + with: + delete-old-comments: true + lcov-file: ./lcov.info + github-token: ${{ secrets.GITHUB_TOKEN }} # Adds a coverage summary comment to the PR. - # - name: Verify minimum coverage - # uses: zgosalvez/github-actions-report-lcov@v2 - # with: - # coverage-files: ./lcov.info - # minimum-coverage: 90 # Set coverage threshold. + - name: Verify minimum coverage + uses: zgosalvez/github-actions-report-lcov@v2 + with: + coverage-files: ./lcov.info + minimum-coverage: 90 # Set coverage threshold. diff --git a/test/unit/Withdraw.t.sol b/test/unit/Withdraw.t.sol index d5da864..6bb8a5a 100644 --- a/test/unit/Withdraw.t.sol +++ b/test/unit/Withdraw.t.sol @@ -291,6 +291,14 @@ contract PSMWithdrawTests is PSMTestBase { ); } + struct WithdrawFuzzTestVars { + uint256 totalUsdc; + uint256 totalValue; + uint256 expectedWithdrawnAmount1; + uint256 expectedWithdrawnAmount2; + uint256 expectedWithdrawnAmount3; + } + // NOTE: For `assertApproxEqAbs` assertions, a difference calculation is used here instead of comparing // the two values because this approach inherently asserts that the shares remaining are lower than the // theoretical value, proving the PSM rounds against the user. @@ -309,90 +317,89 @@ contract PSMWithdrawTests is PSMTestBase { _deposit(address(usdc), user2, depositAmount2); _deposit(address(sDai), user2, depositAmount3); - uint256 totalUsdc = depositAmount1 + depositAmount2; - uint256 totalValue = totalUsdc * 1e12 + depositAmount3 * 125/100; + WithdrawFuzzTestVars memory vars; + + vars.totalUsdc = depositAmount1 + depositAmount2; + vars.totalValue = vars.totalUsdc * 1e12 + depositAmount3 * 125/100; assertEq(usdc.balanceOf(user1), 0); assertEq(usdc.balanceOf(receiver1), 0); - assertEq(usdc.balanceOf(address(psm)), totalUsdc); + assertEq(usdc.balanceOf(address(psm)), vars.totalUsdc); assertEq(psm.shares(user1), depositAmount1 * 1e12); - assertEq(psm.totalShares(), totalValue); + assertEq(psm.totalShares(), vars.totalValue); - uint256 expectedWithdrawnAmount1 - = _getExpectedWithdrawnAmount(usdc, user1, withdrawAmount1); + vars.expectedWithdrawnAmount1 = _getExpectedWithdrawnAmount(usdc, user1, withdrawAmount1); vm.prank(user1); uint256 amount = psm.withdraw(address(usdc), receiver1, withdrawAmount1); - assertEq(amount, expectedWithdrawnAmount1); + assertEq(amount, vars.expectedWithdrawnAmount1); _checkPsmInvariant(); assertEq( usdc.balanceOf(receiver1) * 1e12 + psm.getPsmTotalValue(), - totalValue + vars.totalValue ); // NOTE: User 1 doesn't need a tolerance because their shares are 1e6 precision because they only // deposited USDC. User 2 has a tolerance because they deposited sDAI which has 1e18 precision // so there is a chance that the rounding will be off by up to 1e12. assertEq(usdc.balanceOf(user1), 0); - assertEq(usdc.balanceOf(receiver1), expectedWithdrawnAmount1); + assertEq(usdc.balanceOf(receiver1), vars.expectedWithdrawnAmount1); assertEq(usdc.balanceOf(user2), 0); assertEq(usdc.balanceOf(receiver2), 0); - assertEq(usdc.balanceOf(address(psm)), totalUsdc - expectedWithdrawnAmount1); + assertEq(usdc.balanceOf(address(psm)), vars.totalUsdc - vars.expectedWithdrawnAmount1); - assertEq(psm.shares(user1), (depositAmount1 - expectedWithdrawnAmount1) * 1e12); + assertEq(psm.shares(user1), (depositAmount1 - vars.expectedWithdrawnAmount1) * 1e12); assertEq(psm.shares(user2), depositAmount2 * 1e12 + depositAmount3 * 125/100); // Includes sDAI deposit - assertEq(psm.totalShares(), totalValue - expectedWithdrawnAmount1 * 1e12); + assertEq(psm.totalShares(), vars.totalValue - vars.expectedWithdrawnAmount1 * 1e12); - uint256 expectedWithdrawnAmount2 - = _getExpectedWithdrawnAmount(usdc, user2, withdrawAmount2); + vars.expectedWithdrawnAmount2 = _getExpectedWithdrawnAmount(usdc, user2, withdrawAmount2); vm.prank(user2); amount = psm.withdraw(address(usdc), receiver2, withdrawAmount2); - assertEq(amount, expectedWithdrawnAmount2); + assertEq(amount, vars.expectedWithdrawnAmount2); _checkPsmInvariant(); assertEq( (usdc.balanceOf(receiver1) + usdc.balanceOf(receiver2)) * 1e12 + psm.getPsmTotalValue(), - totalValue + vars.totalValue ); assertEq(usdc.balanceOf(user1), 0); - assertEq(usdc.balanceOf(receiver1), expectedWithdrawnAmount1); + assertEq(usdc.balanceOf(receiver1), vars.expectedWithdrawnAmount1); assertEq(usdc.balanceOf(user2), 0); - assertEq(usdc.balanceOf(receiver2), expectedWithdrawnAmount2); - assertEq(usdc.balanceOf(address(psm)), totalUsdc - (expectedWithdrawnAmount1 + expectedWithdrawnAmount2)); + assertEq(usdc.balanceOf(receiver2), vars.expectedWithdrawnAmount2); + assertEq(usdc.balanceOf(address(psm)), vars.totalUsdc - (vars.expectedWithdrawnAmount1 + vars.expectedWithdrawnAmount2)); assertEq(sDai.balanceOf(user2), 0); assertEq(sDai.balanceOf(receiver2), 0); assertEq(sDai.balanceOf(address(psm)), depositAmount3); - assertEq(psm.shares(user1), (depositAmount1 - expectedWithdrawnAmount1) * 1e12); + assertEq(psm.shares(user1), (depositAmount1 - vars.expectedWithdrawnAmount1) * 1e12); assertApproxEqAbs( - ((depositAmount2 * 1e12) + (depositAmount3 * 125/100) - (expectedWithdrawnAmount2 * 1e12)) - psm.shares(user2), + ((depositAmount2 * 1e12) + (depositAmount3 * 125/100) - (vars.expectedWithdrawnAmount2 * 1e12)) - psm.shares(user2), 0, usdcShareTolerance ); assertApproxEqAbs( - (totalValue - (expectedWithdrawnAmount1 + expectedWithdrawnAmount2) * 1e12) - psm.totalShares(), + (vars.totalValue - (vars.expectedWithdrawnAmount1 + vars.expectedWithdrawnAmount2) * 1e12) - psm.totalShares(), 0, usdcShareTolerance ); - uint256 expectedWithdrawnAmount3 - = _getExpectedWithdrawnAmount(sDai, user2, withdrawAmount3); + vars.expectedWithdrawnAmount3 = _getExpectedWithdrawnAmount(sDai, user2, withdrawAmount3); vm.prank(user2); amount = psm.withdraw(address(sDai), receiver2, withdrawAmount3); - assertApproxEqAbs(amount, expectedWithdrawnAmount3, 1); + assertApproxEqAbs(amount, vars.expectedWithdrawnAmount3, 1); _checkPsmInvariant(); @@ -400,30 +407,30 @@ contract PSMWithdrawTests is PSMTestBase { (usdc.balanceOf(receiver1) + usdc.balanceOf(receiver2)) * 1e12 + (sDai.balanceOf(receiver2) * rateProvider.getConversionRate() / 1e27) + psm.getPsmTotalValue(), - totalValue, + vars.totalValue, 1 ); assertEq(usdc.balanceOf(user1), 0); - assertEq(usdc.balanceOf(receiver1), expectedWithdrawnAmount1); + assertEq(usdc.balanceOf(receiver1), vars.expectedWithdrawnAmount1); assertEq(usdc.balanceOf(user2), 0); - assertEq(usdc.balanceOf(receiver2), expectedWithdrawnAmount2); - assertEq(usdc.balanceOf(address(psm)), totalUsdc - (expectedWithdrawnAmount1 + expectedWithdrawnAmount2)); + assertEq(usdc.balanceOf(receiver2), vars.expectedWithdrawnAmount2); + assertEq(usdc.balanceOf(address(psm)), vars.totalUsdc - (vars.expectedWithdrawnAmount1 + vars.expectedWithdrawnAmount2)); assertApproxEqAbs(sDai.balanceOf(user2), 0, 0); - assertApproxEqAbs(sDai.balanceOf(receiver2), expectedWithdrawnAmount3, 1); - assertApproxEqAbs(sDai.balanceOf(address(psm)), depositAmount3 - expectedWithdrawnAmount3, 1); + assertApproxEqAbs(sDai.balanceOf(receiver2), vars.expectedWithdrawnAmount3, 1); + assertApproxEqAbs(sDai.balanceOf(address(psm)), depositAmount3 - vars.expectedWithdrawnAmount3, 1); - assertEq(psm.shares(user1), (depositAmount1 - expectedWithdrawnAmount1) * 1e12); + assertEq(psm.shares(user1), (depositAmount1 - vars.expectedWithdrawnAmount1) * 1e12); assertApproxEqAbs( - ((depositAmount2 * 1e12) + (depositAmount3 * 125/100) - (expectedWithdrawnAmount2 * 1e12) - (expectedWithdrawnAmount3 * 125/100)) - psm.shares(user2), + ((depositAmount2 * 1e12) + (depositAmount3 * 125/100) - (vars.expectedWithdrawnAmount2 * 1e12) - (vars.expectedWithdrawnAmount3 * 125/100)) - psm.shares(user2), 0, usdcShareTolerance + 1 // 1 is added to the tolerance because of rounding error in sDAI calculations ); assertApproxEqAbs( - totalValue - (expectedWithdrawnAmount1 + expectedWithdrawnAmount2) * 1e12 - (expectedWithdrawnAmount3 * 125/100) - psm.totalShares(), + vars.totalValue - (vars.expectedWithdrawnAmount1 + vars.expectedWithdrawnAmount2) * 1e12 - (vars.expectedWithdrawnAmount3 * 125/100) - psm.totalShares(), 0, usdcShareTolerance + 1 // 1 is added to the tolerance because of rounding error in sDAI calculations ); @@ -563,25 +570,28 @@ contract PSMWithdrawTests is PSMTestBase { vm.prank(user1); amount = psm.withdraw(address(sDai), user1, type(uint256).max); - // User1s remaining shares are used - uint256 user1SDai = (user1Shares - expectedUser1SharesBurned) + + { + // User1s remaining shares are used + uint256 user1SDai = (user1Shares - expectedUser1SharesBurned) * totalValue / totalShares * 1e27 / conversionRate; - assertApproxEqAbs(sDai.balanceOf(user1), user1SDai, 2); - assertApproxEqAbs(sDai.balanceOf(user2), 0, 0); - assertApproxEqAbs(sDai.balanceOf(address(psm)), sDaiAmount - user1SDai, 2); + assertApproxEqAbs(sDai.balanceOf(user1), user1SDai, 2); + assertApproxEqAbs(sDai.balanceOf(user2), 0, 0); + assertApproxEqAbs(sDai.balanceOf(address(psm)), sDaiAmount - user1SDai, 2); - vm.prank(user2); - amount = psm.withdraw(address(sDai), user2, type(uint256).max); + vm.prank(user2); + amount = psm.withdraw(address(sDai), user2, type(uint256).max); - assertApproxEqAbs(amount, sDaiAmount - user1SDai, 2); + assertApproxEqAbs(amount, sDaiAmount - user1SDai, 2); - assertApproxEqAbs(sDai.balanceOf(user1), user1SDai, 2); - assertApproxEqAbs(sDai.balanceOf(user2), sDaiAmount - user1SDai, 2); - assertApproxEqAbs(sDai.balanceOf(address(psm)), 0, 2); + assertApproxEqAbs(sDai.balanceOf(user1), user1SDai, 2); + assertApproxEqAbs(sDai.balanceOf(user2), sDaiAmount - user1SDai, 2); + assertApproxEqAbs(sDai.balanceOf(address(psm)), 0, 2); + } assertLe(psm.totalShares(), 1); assertLe(psm.shares(user1), 1); From 02074d089357a7462a042eb4e485da24e7885dd5 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Thu, 4 Jul 2024 07:56:18 -0400 Subject: [PATCH 06/21] feat: get new swap tests passing --- test/unit/SwapExactOut.t.sol | 76 ++++++++++++++++++------------------ 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/test/unit/SwapExactOut.t.sol b/test/unit/SwapExactOut.t.sol index 6b3693d..ac1d0cb 100644 --- a/test/unit/SwapExactOut.t.sol +++ b/test/unit/SwapExactOut.t.sol @@ -137,8 +137,8 @@ contract PSMSwapExactOutSuccessTestsBase is PSMTestBase { function _swapExactOutTest( MockERC20 assetIn, MockERC20 assetOut, - uint256 amountIn, uint256 amountOut, + uint256 amountIn, address swapper_, address receiver_ ) internal { @@ -160,7 +160,7 @@ contract PSMSwapExactOutSuccessTestsBase is PSMTestBase { assertEq(assetOut.balanceOf(receiver_), 0); assertEq(assetOut.balanceOf(address(psm)), psmAssetOutBalance); - psm.swapExactOut(address(assetIn), address(assetOut), amountIn, amountOut, receiver_, 0); + psm.swapExactOut(address(assetIn), address(assetOut), amountOut, amountIn, receiver_, 0); assertEq(assetIn.allowance(swapper_, address(psm)), 0); @@ -176,23 +176,23 @@ contract PSMSwapExactOutSuccessTestsBase is PSMTestBase { contract PSMSwapExactOutDaiAssetInTests is PSMSwapExactOutSuccessTestsBase { function test_swapExactOut_daiToUsdc_sameReceiver() public assertAtomicPsmValueDoesNotChange { - _swapExactOutTest(dai, usdc, 100e18, 100e6, swapper, swapper); + _swapExactOutTest(dai, usdc, 100e6, 100e18, swapper, swapper); } function test_swapExactOut_daiToSDai_sameReceiver() public assertAtomicPsmValueDoesNotChange { - _swapExactOutTest(dai, sDai, 100e18, 80e18, swapper, swapper); + _swapExactOutTest(dai, sDai, 80e18, 100e18, swapper, swapper); } function test_swapExactOut_daiToUsdc_differentReceiver() public assertAtomicPsmValueDoesNotChange { - _swapExactOutTest(dai, usdc, 100e18, 100e6, swapper, receiver); + _swapExactOutTest(dai, usdc, 100e6, 100e18, swapper, receiver); } function test_swapExactOut_daiToSDai_differentReceiver() public assertAtomicPsmValueDoesNotChange { - _swapExactOutTest(dai, sDai, 100e18, 80e18, swapper, receiver); + _swapExactOutTest(dai, sDai, 80e18, 100e18, swapper, receiver); } function testFuzz_swapExactOut_daiToUsdc( - uint256 amountIn, + uint256 amountOut, address fuzzSwapper, address fuzzReceiver ) public { @@ -200,13 +200,13 @@ contract PSMSwapExactOutDaiAssetInTests is PSMSwapExactOutSuccessTestsBase { vm.assume(fuzzReceiver != address(psm)); vm.assume(fuzzReceiver != address(0)); - amountIn = _bound(amountIn, 1, DAI_TOKEN_MAX); // Zero amount reverts - uint256 amountOut = amountIn / 1e12; - _swapExactOutTest(dai, usdc, amountIn, amountOut, fuzzSwapper, fuzzReceiver); + amountOut = _bound(amountOut, 1, USDC_TOKEN_MAX); // Zero amount reverts + uint256 amountIn = amountOut * 1e12; + _swapExactOutTest(dai, usdc, amountOut, amountIn, fuzzSwapper, fuzzReceiver); } function testFuzz_swapExactOut_daiToSDai( - uint256 amountIn, + uint256 amountOut, uint256 conversionRate, address fuzzSwapper, address fuzzReceiver @@ -215,13 +215,13 @@ contract PSMSwapExactOutDaiAssetInTests is PSMSwapExactOutSuccessTestsBase { vm.assume(fuzzReceiver != address(psm)); vm.assume(fuzzReceiver != address(0)); - amountIn = _bound(amountIn, 1, DAI_TOKEN_MAX); + amountOut = _bound(amountOut, 1, DAI_TOKEN_MAX); conversionRate = _bound(conversionRate, 0.01e27, 100e27); // 1% to 10,000% conversion rate rateProvider.__setConversionRate(conversionRate); - uint256 amountOut = amountIn * 1e27 / conversionRate; + uint256 amountIn = amountOut * conversionRate / 1e27; - _swapExactOutTest(dai, sDai, amountIn, amountOut, fuzzSwapper, fuzzReceiver); + _swapExactOutTest(dai, sDai, amountOut, amountIn, fuzzSwapper, fuzzReceiver); } } @@ -229,23 +229,23 @@ contract PSMSwapExactOutDaiAssetInTests is PSMSwapExactOutSuccessTestsBase { contract PSMSwapExactOutUsdcAssetInTests is PSMSwapExactOutSuccessTestsBase { function test_swapExactOut_usdcToDai_sameReceiver() public assertAtomicPsmValueDoesNotChange { - _swapExactOutTest(usdc, dai, 100e6, 100e18, swapper, swapper); + _swapExactOutTest(usdc, dai, 100e18, 100e6, swapper, swapper); } function test_swapExactOut_usdcToSDai_sameReceiver() public assertAtomicPsmValueDoesNotChange { - _swapExactOutTest(usdc, sDai, 100e6, 80e18, swapper, swapper); + _swapExactOutTest(usdc, sDai, 80e18, 100e6, swapper, swapper); } function test_swapExactOut_usdcToDai_differentReceiver() public assertAtomicPsmValueDoesNotChange { - _swapExactOutTest(usdc, dai, 100e6, 100e18, swapper, receiver); + _swapExactOutTest(usdc, dai, 100e18, 100e6, swapper, receiver); } function test_swapExactOut_usdcToSDai_differentReceiver() public assertAtomicPsmValueDoesNotChange { - _swapExactOutTest(usdc, sDai, 100e6, 80e18, swapper, receiver); + _swapExactOutTest(usdc, sDai, 80e18, 100e6, swapper, receiver); } function testFuzz_swapExactOut_usdcToDai( - uint256 amountIn, + uint256 amountOut, address fuzzSwapper, address fuzzReceiver ) public { @@ -253,13 +253,13 @@ contract PSMSwapExactOutUsdcAssetInTests is PSMSwapExactOutSuccessTestsBase { vm.assume(fuzzReceiver != address(psm)); vm.assume(fuzzReceiver != address(0)); - amountIn = _bound(amountIn, 1, USDC_TOKEN_MAX); // Zero amount reverts - uint256 amountOut = amountIn * 1e12; - _swapExactOutTest(usdc, dai, amountIn, amountOut, fuzzSwapper, fuzzReceiver); + amountOut = _bound(amountOut, 1, DAI_TOKEN_MAX); // Zero amount reverts + uint256 amountIn = amountOut / 1e12; + _swapExactOutTest(usdc, dai, amountOut, amountIn, fuzzSwapper, fuzzReceiver); } function testFuzz_swapExactOut_usdcToSDai( - uint256 amountIn, + uint256 amountOut, uint256 conversionRate, address fuzzSwapper, address fuzzReceiver @@ -268,14 +268,14 @@ contract PSMSwapExactOutUsdcAssetInTests is PSMSwapExactOutSuccessTestsBase { vm.assume(fuzzReceiver != address(psm)); vm.assume(fuzzReceiver != address(0)); - amountIn = _bound(amountIn, 1, USDC_TOKEN_MAX); + amountOut = _bound(amountOut, 1, SDAI_TOKEN_MAX); conversionRate = _bound(conversionRate, 0.01e27, 100e27); // 1% to 10,000% conversion rate rateProvider.__setConversionRate(conversionRate); - uint256 amountOut = amountIn * 1e27 / conversionRate * 1e12; + uint256 amountIn = amountOut * conversionRate / 1e27 / 1e12; - _swapExactOutTest(usdc, sDai, amountIn, amountOut, fuzzSwapper, fuzzReceiver); + _swapExactOutTest(usdc, sDai, amountOut, amountIn, fuzzSwapper, fuzzReceiver); } } @@ -283,23 +283,23 @@ contract PSMSwapExactOutUsdcAssetInTests is PSMSwapExactOutSuccessTestsBase { contract PSMSwapExactOutSDaiAssetInTests is PSMSwapExactOutSuccessTestsBase { function test_swapExactOut_sDaiToDai_sameReceiver() public assertAtomicPsmValueDoesNotChange { - _swapExactOutTest(sDai, dai, 100e18, 125e18, swapper, swapper); + _swapExactOutTest(sDai, dai, 125e18, 100e18, swapper, swapper); } function test_swapExactOut_sDaiToUsdc_sameReceiver() public assertAtomicPsmValueDoesNotChange { - _swapExactOutTest(sDai, usdc, 100e18, 125e6, swapper, swapper); + _swapExactOutTest(sDai, usdc, 125e6, 100e18, swapper, swapper); } function test_swapExactOut_sDaiToDai_differentReceiver() public assertAtomicPsmValueDoesNotChange { - _swapExactOutTest(sDai, dai, 100e18, 125e18, swapper, receiver); + _swapExactOutTest(sDai, dai, 125e18, 100e18, swapper, receiver); } function test_swapExactOut_sDaiToUsdc_differentReceiver() public assertAtomicPsmValueDoesNotChange { - _swapExactOutTest(sDai, usdc, 100e18, 125e6, swapper, receiver); + _swapExactOutTest(sDai, usdc, 125e6, 100e18, swapper, receiver); } function testFuzz_swapExactOut_sDaiToDai( - uint256 amountIn, + uint256 amountOut, uint256 conversionRate, address fuzzSwapper, address fuzzReceiver @@ -308,18 +308,18 @@ contract PSMSwapExactOutSDaiAssetInTests is PSMSwapExactOutSuccessTestsBase { vm.assume(fuzzReceiver != address(psm)); vm.assume(fuzzReceiver != address(0)); - amountIn = _bound(amountIn, 1, SDAI_TOKEN_MAX); + amountOut = _bound(amountOut, 1, DAI_TOKEN_MAX); conversionRate = _bound(conversionRate, 0.01e27, 100e27); // 1% to 10,000% conversion rate rateProvider.__setConversionRate(conversionRate); - uint256 amountOut = amountIn * conversionRate / 1e27; + uint256 amountIn = amountOut * 1e27 / conversionRate; - _swapExactOutTest(sDai, dai, amountIn, amountOut, fuzzSwapper, fuzzReceiver); + _swapExactOutTest(sDai, dai, amountOut, amountIn, fuzzSwapper, fuzzReceiver); } function testFuzz_swapExactOut_sDaiToUsdc( - uint256 amountIn, + uint256 amountOut, uint256 conversionRate, address fuzzSwapper, address fuzzReceiver @@ -328,14 +328,14 @@ contract PSMSwapExactOutSDaiAssetInTests is PSMSwapExactOutSuccessTestsBase { vm.assume(fuzzReceiver != address(psm)); vm.assume(fuzzReceiver != address(0)); - amountIn = _bound(amountIn, 1, SDAI_TOKEN_MAX); + amountOut = _bound(amountOut, 1, USDC_TOKEN_MAX); conversionRate = _bound(conversionRate, 0.01e27, 100e27); // 1% to 10,000% conversion rate rateProvider.__setConversionRate(conversionRate); - uint256 amountOut = amountIn * conversionRate / 1e27 / 1e12; + uint256 amountIn = amountOut * 1e27 / conversionRate * 1e12; - _swapExactOutTest(sDai, usdc, amountIn, amountOut, fuzzSwapper, fuzzReceiver); + _swapExactOutTest(sDai, usdc, amountOut, amountIn, fuzzSwapper, fuzzReceiver); } } From 93220d54d4dc924df6650e8e1ead84f184838500 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Thu, 4 Jul 2024 08:37:35 -0400 Subject: [PATCH 07/21] feat: set up initial test --- test/unit/SwapExactIn.t.sol | 66 +++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/test/unit/SwapExactIn.t.sol b/test/unit/SwapExactIn.t.sol index 92389e0..38d8ab2 100644 --- a/test/unit/SwapExactIn.t.sol +++ b/test/unit/SwapExactIn.t.sol @@ -344,3 +344,69 @@ contract PSMSwapExactInSDaiAssetInTests is PSMSwapExactInSuccessTestsBase { } } + +contract PSMSwapExactInFuzzTests is PSMTestBase { + + address lp0 = makeAddr("lp0"); + address lp1 = makeAddr("lp1"); + address lp2 = makeAddr("lp2"); + + address swapper = makeAddr("swapper"); + + function _hash(uint256 number_, string memory salt) internal pure returns (uint256 hash_) { + hash_ = uint256(keccak256(abi.encode(number_, salt))); + } + + function _getAsset(uint256 indexSeed) internal view returns (MockERC20) { + uint256 index = indexSeed % 3; + + if (index == 0) return dai; + if (index == 1) return usdc; + if (index == 2) return sDai; + } + + /// forge-config: default.fuzz.runs = 1 + function testFuzz_swapExactIn( + uint256 conversionRate, + uint256 depositSeed + ) public { + // 1. LPs deposit fuzzed amounts of all tokens + // 2. 1000 swaps happen + // 3. Check that the LPs have the same balances + // 4. Check that the PSM has the same value + + conversionRate = _bound(conversionRate, 0.01e27, 100e27); // 1% to 10,000% conversion rate + + rateProvider.__setConversionRate(conversionRate); + + _deposit(address(dai), lp0, _bound(_hash(depositSeed, "lp0-dai"), 1, DAI_TOKEN_MAX)); + + _deposit(address(usdc), lp1, _bound(_hash(depositSeed, "lp1-usdc"), 1, USDC_TOKEN_MAX)); + _deposit(address(sDai), lp1, _bound(_hash(depositSeed, "lp1-sdai"), 1, SDAI_TOKEN_MAX)); + + _deposit(address(dai), lp2, _bound(_hash(depositSeed, "lp2-dai"), 1, DAI_TOKEN_MAX)); + _deposit(address(usdc), lp2, _bound(_hash(depositSeed, "lp2-usdc"), 1, USDC_TOKEN_MAX)); + _deposit(address(sDai), lp2, _bound(_hash(depositSeed, "lp2-sdai"), 1, SDAI_TOKEN_MAX)); + + vm.startPrank(swapper); + + for (uint256 i; i < 1000; ++i) { + MockERC20 assetIn = _getAsset(_hash(i, "assetIn")); + MockERC20 assetOut = _getAsset(_hash(i, "assetOut")); + + // Calculate the maximum amount that can be swapped by using the inverse conversion rate + uint256 maxAmountIn = psm.previewSwapExactOut( + address(assetOut), + address(assetIn), + assetOut.balanceOf(address(psm)) + ); + + uint256 amountIn = _bound(_hash(i, "amountIn"), 0, maxAmountIn); + + assetIn.mint(swapper, amountIn); + assetIn.approve(address(psm), amountIn); + psm.swapExactIn(address(assetIn), address(assetOut), amountIn, 0, swapper, 0); + } + + } +} From 7a9c76e75f731bc29e221edf8e6767b14d3ec8de Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Thu, 4 Jul 2024 08:42:55 -0400 Subject: [PATCH 08/21] feat: refactor code to add returns --- src/PSM3.sol | 8 +++---- src/interfaces/IPSM3.sol | 50 +++++++++++++++++++++------------------- 2 files changed, 30 insertions(+), 28 deletions(-) diff --git a/src/PSM3.sol b/src/PSM3.sol index 81162ad..867a3ca 100644 --- a/src/PSM3.sol +++ b/src/PSM3.sol @@ -64,12 +64,12 @@ contract PSM3 is IPSM3 { address receiver, uint256 referralCode ) - external override + external override returns (uint256 amountOut) { require(amountIn != 0, "PSM3/invalid-amountIn"); require(receiver != address(0), "PSM3/invalid-receiver"); - uint256 amountOut = previewSwapExactIn(assetIn, assetOut, amountIn); + amountOut = previewSwapExactIn(assetIn, assetOut, amountIn); require(amountOut >= minAmountOut, "PSM3/amountOut-too-low"); @@ -87,12 +87,12 @@ contract PSM3 is IPSM3 { address receiver, uint256 referralCode ) - external override + external override returns (uint256 amountIn) { require(amountOut != 0, "PSM3/invalid-amountOut"); require(receiver != address(0), "PSM3/invalid-receiver"); - uint256 amountIn = previewSwapExactOut(assetIn, assetOut, amountOut); + amountIn = previewSwapExactOut(assetIn, assetOut, amountOut); require(amountIn <= maxAmountIn, "PSM3/amountIn-too-high"); diff --git a/src/interfaces/IPSM3.sol b/src/interfaces/IPSM3.sol index a22a31a..fc2f1a6 100644 --- a/src/interfaces/IPSM3.sol +++ b/src/interfaces/IPSM3.sol @@ -113,16 +113,17 @@ interface IPSM3 { /**********************************************************************************************/ /** - * @dev Swaps a specified amount of assetIn for assetOut in the PSM. The amount swapped is - * converted based on the current value of the two assets used in the swap. This - * function will revert if there is not enough balance in the PSM to facilitate the - * swap. Both assets must be supported in the PSM in order to succeed. - * @param assetIn Address of the ERC-20 asset to swap in. - * @param assetOut Address of the ERC-20 asset to swap out. - * @param amountIn Amount of the asset to swap in. - * @param minAmountOut Minimum amount of the asset to receive. - * @param receiver Address of the receiver of the swapped assets. - * @param referralCode Referral code for the swap. + * @dev Swaps a specified amount of assetIn for assetOut in the PSM. The amount swapped is + * converted based on the current value of the two assets used in the swap. This + * function will revert if there is not enough balance in the PSM to facilitate the + * swap. Both assets must be supported in the PSM in order to succeed. + * @param assetIn Address of the ERC-20 asset to swap in. + * @param assetOut Address of the ERC-20 asset to swap out. + * @param amountIn Amount of the asset to swap in. + * @param minAmountOut Minimum amount of the asset to receive. + * @param receiver Address of the receiver of the swapped assets. + * @param referralCode Referral code for the swap. + * @return amountOut Resulting mount of the asset that will be received in the swap. */ function swapExactIn( address assetIn, @@ -131,28 +132,29 @@ interface IPSM3 { uint256 minAmountOut, address receiver, uint256 referralCode - ) external; + ) external returns (uint256 amountOut); /** - * @dev Swaps a derived amount of assetIn for a specific amount of assetOut in the PSM. The - * amount swapped is converted based on the current value of the two assets used in - * the swap. This function will revert if there is not enough balance in the PSM to - * facilitate the swap. Both assets must be supported in the PSM in order to succeed. - * @param assetIn Address of the ERC-20 asset to swap in. - * @param assetOut Address of the ERC-20 asset to swap out. - * @param amountIn Amount of the asset to swap in. - * @param minAmountOut Minimum amount of the asset to receive. - * @param receiver Address of the receiver of the swapped assets. - * @param referralCode Referral code for the swap. + * @dev Swaps a derived amount of assetIn for a specific amount of assetOut in the PSM. The + * amount swapped is converted based on the current value of the two assets used in + * the swap. This function will revert if there is not enough balance in the PSM to + * facilitate the swap. Both assets must be supported in the PSM in order to succeed. + * @param assetIn Address of the ERC-20 asset to swap in. + * @param assetOut Address of the ERC-20 asset to swap out. + * @param amountOut Amount of the asset to receive from the swap. + * @param maxAmountIn Max amount of the asset to use for the swap. + * @param receiver Address of the receiver of the swapped assets. + * @param referralCode Referral code for the swap. + * @return amountIn Resulting amount of the asset swapped in. */ function swapExactOut( address assetIn, address assetOut, - uint256 amountIn, - uint256 minAmountOut, + uint256 amountOut, + uint256 maxAmountIn, address receiver, uint256 referralCode - ) external; + ) external returns (uint256 amountIn); /**********************************************************************************************/ /*** Liquidity provision functions ***/ From 82798561671c748b8a66336df40f27336c16a7ab Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Thu, 4 Jul 2024 08:46:07 -0400 Subject: [PATCH 09/21] feat: add return value assertions --- test/unit/SwapExactIn.t.sol | 11 ++++++++++- test/unit/SwapExactOut.t.sol | 11 ++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/test/unit/SwapExactIn.t.sol b/test/unit/SwapExactIn.t.sol index 92389e0..3390b65 100644 --- a/test/unit/SwapExactIn.t.sol +++ b/test/unit/SwapExactIn.t.sol @@ -165,7 +165,16 @@ contract PSMSwapExactInSuccessTestsBase is PSMTestBase { assertEq(assetOut.balanceOf(receiver_), 0); assertEq(assetOut.balanceOf(address(psm)), psmAssetOutBalance); - psm.swapExactIn(address(assetIn), address(assetOut), amountIn, amountOut, receiver_, 0); + uint256 returnedAmountOut = psm.swapExactIn( + address(assetIn), + address(assetOut), + amountIn, + amountOut, + receiver_, + 0 + ); + + assertEq(returnedAmountOut, amountOut); assertEq(assetIn.allowance(swapper_, address(psm)), 0); diff --git a/test/unit/SwapExactOut.t.sol b/test/unit/SwapExactOut.t.sol index ac1d0cb..f2eec3b 100644 --- a/test/unit/SwapExactOut.t.sol +++ b/test/unit/SwapExactOut.t.sol @@ -160,7 +160,16 @@ contract PSMSwapExactOutSuccessTestsBase is PSMTestBase { assertEq(assetOut.balanceOf(receiver_), 0); assertEq(assetOut.balanceOf(address(psm)), psmAssetOutBalance); - psm.swapExactOut(address(assetIn), address(assetOut), amountOut, amountIn, receiver_, 0); + uint256 returnedAmountIn = psm.swapExactOut( + address(assetIn), + address(assetOut), + amountOut, + amountIn, + receiver_, + 0) + ; + + assertEq(returnedAmountIn, amountIn); assertEq(assetIn.allowance(swapper_, address(psm)), 0); From 2aabbf841dc10d1c833ffcee05ce254dd93bce58 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Thu, 4 Jul 2024 09:03:51 -0400 Subject: [PATCH 10/21] feat: working fuzz test --- test/unit/SwapExactIn.t.sol | 70 +++++++++++++++++++++++++++++++------ 1 file changed, 59 insertions(+), 11 deletions(-) diff --git a/test/unit/SwapExactIn.t.sol b/test/unit/SwapExactIn.t.sol index e6e4d34..bf44cb3 100644 --- a/test/unit/SwapExactIn.t.sol +++ b/test/unit/SwapExactIn.t.sol @@ -372,21 +372,30 @@ contract PSMSwapExactInFuzzTests is PSMTestBase { if (index == 0) return dai; if (index == 1) return usdc; if (index == 2) return sDai; + + else revert("Invalid index"); + } + + struct FuzzVars { + uint256 lp0StartingValue; + uint256 lp1StartingValue; + uint256 lp2StartingValue; + uint256 psmStartingValue; + uint256 lp0CachedValue; + uint256 lp1CachedValue; + uint256 lp2CachedValue; + uint256 psmCachedValue; } - /// forge-config: default.fuzz.runs = 1 + /// forge-config: default.fuzz.runs = 10 + /// forge-config: pr.fuzz.runs = 100 + /// forge-config: master.fuzz.runs = 10000 function testFuzz_swapExactIn( uint256 conversionRate, uint256 depositSeed ) public { - // 1. LPs deposit fuzzed amounts of all tokens - // 2. 1000 swaps happen - // 3. Check that the LPs have the same balances - // 4. Check that the PSM has the same value - - conversionRate = _bound(conversionRate, 0.01e27, 100e27); // 1% to 10,000% conversion rate - - rateProvider.__setConversionRate(conversionRate); + // 1% to 200% conversion rate + rateProvider.__setConversionRate(_bound(conversionRate, 0.01e27, 2e27)); _deposit(address(dai), lp0, _bound(_hash(depositSeed, "lp0-dai"), 1, DAI_TOKEN_MAX)); @@ -397,25 +406,64 @@ contract PSMSwapExactInFuzzTests is PSMTestBase { _deposit(address(usdc), lp2, _bound(_hash(depositSeed, "lp2-usdc"), 1, USDC_TOKEN_MAX)); _deposit(address(sDai), lp2, _bound(_hash(depositSeed, "lp2-sdai"), 1, SDAI_TOKEN_MAX)); + FuzzVars memory vars; + + vars.lp0StartingValue = psm.convertToAssetValue(psm.shares(lp0)); + vars.lp1StartingValue = psm.convertToAssetValue(psm.shares(lp1)); + vars.lp2StartingValue = psm.convertToAssetValue(psm.shares(lp2)); + vars.psmStartingValue = psm.getPsmTotalValue(); + vm.startPrank(swapper); for (uint256 i; i < 1000; ++i) { MockERC20 assetIn = _getAsset(_hash(i, "assetIn")); MockERC20 assetOut = _getAsset(_hash(i, "assetOut")); + if (assetIn == assetOut) { + assetOut = _getAsset(_hash(i, "assetOut") + 1); + } + // Calculate the maximum amount that can be swapped by using the inverse conversion rate uint256 maxAmountIn = psm.previewSwapExactOut( - address(assetOut), address(assetIn), + address(assetOut), assetOut.balanceOf(address(psm)) ); - uint256 amountIn = _bound(_hash(i, "amountIn"), 0, maxAmountIn); + uint256 amountIn = _bound(_hash(i, "amountIn"), 0, maxAmountIn - 1); // Rounding + + vars.lp0CachedValue = psm.convertToAssetValue(psm.shares(lp0)); + vars.lp1CachedValue = psm.convertToAssetValue(psm.shares(lp1)); + vars.lp2CachedValue = psm.convertToAssetValue(psm.shares(lp2)); + vars.psmCachedValue = psm.getPsmTotalValue(); assetIn.mint(swapper, amountIn); assetIn.approve(address(psm), amountIn); psm.swapExactIn(address(assetIn), address(assetOut), amountIn, 0, swapper, 0); + + // Rounding is always in favour of the users + assertGe(psm.convertToAssetValue(psm.shares(lp0)), vars.lp0CachedValue); + assertGe(psm.convertToAssetValue(psm.shares(lp1)), vars.lp1CachedValue); + assertGe(psm.convertToAssetValue(psm.shares(lp2)), vars.lp2CachedValue); + assertGe(psm.getPsmTotalValue(), vars.psmCachedValue); + + // Up to 2e12 rounding on each swap + assertApproxEqAbs(psm.convertToAssetValue(psm.shares(lp0)), vars.lp0CachedValue, 2e12); + assertApproxEqAbs(psm.convertToAssetValue(psm.shares(lp1)), vars.lp1CachedValue, 2e12); + assertApproxEqAbs(psm.convertToAssetValue(psm.shares(lp2)), vars.lp2CachedValue, 2e12); + assertApproxEqAbs(psm.getPsmTotalValue(), vars.psmCachedValue, 2e12); } + // Rounding is always in favour of the users + assertGe(psm.convertToAssetValue(psm.shares(lp0)), vars.lp0StartingValue); + assertGe(psm.convertToAssetValue(psm.shares(lp1)), vars.lp1StartingValue); + assertGe(psm.convertToAssetValue(psm.shares(lp2)), vars.lp2StartingValue); + assertGe(psm.getPsmTotalValue(), vars.psmStartingValue); + + // Up to 2e12 rounding on each swap, for 1000 swaps + assertApproxEqAbs(psm.convertToAssetValue(psm.shares(lp0)), vars.lp0StartingValue, 2000e12); + assertApproxEqAbs(psm.convertToAssetValue(psm.shares(lp1)), vars.lp1StartingValue, 2000e12); + assertApproxEqAbs(psm.convertToAssetValue(psm.shares(lp2)), vars.lp2StartingValue, 2000e12); + assertApproxEqAbs(psm.getPsmTotalValue(), vars.psmStartingValue, 2000e12); } } From e810447b4afb40e90d7d0d27d61d620d7c9e0929 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Thu, 4 Jul 2024 09:07:39 -0400 Subject: [PATCH 11/21] feat: add swap exact out, show that rounding is against user --- test/unit/SwapExactIn.t.sol | 29 ++++----- test/unit/SwapExactOut.t.sol | 110 +++++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+), 14 deletions(-) diff --git a/test/unit/SwapExactIn.t.sol b/test/unit/SwapExactIn.t.sol index bf44cb3..bfe8c54 100644 --- a/test/unit/SwapExactIn.t.sol +++ b/test/unit/SwapExactIn.t.sol @@ -362,20 +362,6 @@ contract PSMSwapExactInFuzzTests is PSMTestBase { address swapper = makeAddr("swapper"); - function _hash(uint256 number_, string memory salt) internal pure returns (uint256 hash_) { - hash_ = uint256(keccak256(abi.encode(number_, salt))); - } - - function _getAsset(uint256 indexSeed) internal view returns (MockERC20) { - uint256 index = indexSeed % 3; - - if (index == 0) return dai; - if (index == 1) return usdc; - if (index == 2) return sDai; - - else revert("Invalid index"); - } - struct FuzzVars { uint256 lp0StartingValue; uint256 lp1StartingValue; @@ -466,4 +452,19 @@ contract PSMSwapExactInFuzzTests is PSMTestBase { assertApproxEqAbs(psm.convertToAssetValue(psm.shares(lp2)), vars.lp2StartingValue, 2000e12); assertApproxEqAbs(psm.getPsmTotalValue(), vars.psmStartingValue, 2000e12); } + + function _hash(uint256 number_, string memory salt) internal pure returns (uint256 hash_) { + hash_ = uint256(keccak256(abi.encode(number_, salt))); + } + + function _getAsset(uint256 indexSeed) internal view returns (MockERC20) { + uint256 index = indexSeed % 3; + + if (index == 0) return dai; + if (index == 1) return usdc; + if (index == 2) return sDai; + + else revert("Invalid index"); + } + } diff --git a/test/unit/SwapExactOut.t.sol b/test/unit/SwapExactOut.t.sol index f2eec3b..4b2ec45 100644 --- a/test/unit/SwapExactOut.t.sol +++ b/test/unit/SwapExactOut.t.sol @@ -348,3 +348,113 @@ contract PSMSwapExactOutSDaiAssetInTests is PSMSwapExactOutSuccessTestsBase { } } + +contract PSMSwapExactOutFuzzTests is PSMTestBase { + + address lp0 = makeAddr("lp0"); + address lp1 = makeAddr("lp1"); + address lp2 = makeAddr("lp2"); + + address swapper = makeAddr("swapper"); + + struct FuzzVars { + uint256 lp0StartingValue; + uint256 lp1StartingValue; + uint256 lp2StartingValue; + uint256 psmStartingValue; + uint256 lp0CachedValue; + uint256 lp1CachedValue; + uint256 lp2CachedValue; + uint256 psmCachedValue; + } + + /// forge-config: default.fuzz.runs = 10 + /// forge-config: pr.fuzz.runs = 100 + /// forge-config: master.fuzz.runs = 10000 + function testFuzz_swapExactOut( + uint256 conversionRate, + uint256 depositSeed + ) public { + // 1% to 200% conversion rate + rateProvider.__setConversionRate(_bound(conversionRate, 0.01e27, 2e27)); + + _deposit(address(dai), lp0, _bound(_hash(depositSeed, "lp0-dai"), 1, DAI_TOKEN_MAX)); + + _deposit(address(usdc), lp1, _bound(_hash(depositSeed, "lp1-usdc"), 1, USDC_TOKEN_MAX)); + _deposit(address(sDai), lp1, _bound(_hash(depositSeed, "lp1-sdai"), 1, SDAI_TOKEN_MAX)); + + _deposit(address(dai), lp2, _bound(_hash(depositSeed, "lp2-dai"), 1, DAI_TOKEN_MAX)); + _deposit(address(usdc), lp2, _bound(_hash(depositSeed, "lp2-usdc"), 1, USDC_TOKEN_MAX)); + _deposit(address(sDai), lp2, _bound(_hash(depositSeed, "lp2-sdai"), 1, SDAI_TOKEN_MAX)); + + FuzzVars memory vars; + + vars.lp0StartingValue = psm.convertToAssetValue(psm.shares(lp0)); + vars.lp1StartingValue = psm.convertToAssetValue(psm.shares(lp1)); + vars.lp2StartingValue = psm.convertToAssetValue(psm.shares(lp2)); + vars.psmStartingValue = psm.getPsmTotalValue(); + + vm.startPrank(swapper); + + for (uint256 i; i < 1000; ++i) { + MockERC20 assetIn = _getAsset(_hash(i, "assetIn")); + MockERC20 assetOut = _getAsset(_hash(i, "assetOut")); + + if (assetIn == assetOut) { + assetOut = _getAsset(_hash(i, "assetOut") + 1); + } + + uint256 amountOut = _bound(_hash(i, "amountOut"), 0, assetOut.balanceOf(address(psm))); + + uint256 amountIn = psm.previewSwapExactOut(address(assetIn), address(assetOut), amountOut); + + vars.lp0CachedValue = psm.convertToAssetValue(psm.shares(lp0)); + vars.lp1CachedValue = psm.convertToAssetValue(psm.shares(lp1)); + vars.lp2CachedValue = psm.convertToAssetValue(psm.shares(lp2)); + vars.psmCachedValue = psm.getPsmTotalValue(); + + assetIn.mint(swapper, amountIn); + assetIn.approve(address(psm), amountIn); + psm.swapExactOut(address(assetIn), address(assetOut), amountOut, amountIn, swapper, 0); + + // Rounding is always in favour of the users + assertGe(psm.convertToAssetValue(psm.shares(lp0)), vars.lp0CachedValue); + assertGe(psm.convertToAssetValue(psm.shares(lp1)), vars.lp1CachedValue); + assertGe(psm.convertToAssetValue(psm.shares(lp2)), vars.lp2CachedValue); + assertGe(psm.getPsmTotalValue(), vars.psmCachedValue); + + // Up to 2e12 rounding on each swap + assertApproxEqAbs(psm.convertToAssetValue(psm.shares(lp0)), vars.lp0CachedValue, 2e12); + assertApproxEqAbs(psm.convertToAssetValue(psm.shares(lp1)), vars.lp1CachedValue, 2e12); + assertApproxEqAbs(psm.convertToAssetValue(psm.shares(lp2)), vars.lp2CachedValue, 2e12); + assertApproxEqAbs(psm.getPsmTotalValue(), vars.psmCachedValue, 2e12); + } + + // Rounding is always in favour of the users + assertGe(psm.convertToAssetValue(psm.shares(lp0)), vars.lp0StartingValue); + assertGe(psm.convertToAssetValue(psm.shares(lp1)), vars.lp1StartingValue); + assertGe(psm.convertToAssetValue(psm.shares(lp2)), vars.lp2StartingValue); + assertGe(psm.getPsmTotalValue(), vars.psmStartingValue); + + // Up to 2e12 rounding on each swap, for 1000 swaps + assertApproxEqAbs(psm.convertToAssetValue(psm.shares(lp0)), vars.lp0StartingValue, 2000e12); + assertApproxEqAbs(psm.convertToAssetValue(psm.shares(lp1)), vars.lp1StartingValue, 2000e12); + assertApproxEqAbs(psm.convertToAssetValue(psm.shares(lp2)), vars.lp2StartingValue, 2000e12); + assertApproxEqAbs(psm.getPsmTotalValue(), vars.psmStartingValue, 2000e12); + } + + function _hash(uint256 number_, string memory salt) internal pure returns (uint256 hash_) { + hash_ = uint256(keccak256(abi.encode(number_, salt))); + } + + function _getAsset(uint256 indexSeed) internal view returns (MockERC20) { + uint256 index = indexSeed % 3; + + if (index == 0) return dai; + if (index == 1) return usdc; + if (index == 2) return sDai; + + else revert("Invalid index"); + } + +} From 348443301500b667ce8526c5ad59881d4fdc1919 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Thu, 4 Jul 2024 09:09:53 -0400 Subject: [PATCH 12/21] fix: update natspec --- src/interfaces/IPSM3.sol | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/interfaces/IPSM3.sol b/src/interfaces/IPSM3.sol index fc2f1a6..4f02ca4 100644 --- a/src/interfaces/IPSM3.sol +++ b/src/interfaces/IPSM3.sol @@ -231,7 +231,15 @@ interface IPSM3 { function previewSwapExactIn(address assetIn, address assetOut, uint256 amountIn) external view returns (uint256 amountOut); - // TODO + /** + * @dev View function that returns the exact amount of assetIn that would be required to + * receive a given amount of assetOut in a swap. The amount returned is + * converted based on the current value of the two assets used in the swap. + * @param assetIn Address of the ERC-20 asset to swap in. + * @param assetOut Address of the ERC-20 asset to swap out. + * @param amountOut Amount of the asset to receive from the swap. + * @return amountIn Amount of the asset that is required to receive amountOut. + */ function previewSwapExactOut(address assetIn, address assetOut, uint256 amountOut) external view returns (uint256 amountIn); From d736ad1fcb231e6b321700b488a301fb3a3ae112 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Thu, 4 Jul 2024 10:14:36 -0400 Subject: [PATCH 13/21] feat: refactor to round up on swap exact out --- src/PSM3.sol | 60 +++++++++++++++++++++--------------- test/unit/Previews.t.sol | 17 +++++++--- test/unit/SwapExactOut.t.sol | 52 ++++++++++++++++++++++++++----- 3 files changed, 91 insertions(+), 38 deletions(-) diff --git a/src/PSM3.sol b/src/PSM3.sol index 867a3ca..6e3bde8 100644 --- a/src/PSM3.sol +++ b/src/PSM3.sol @@ -183,13 +183,15 @@ contract PSM3 is IPSM3 { function previewSwapExactIn(address assetIn, address assetOut, uint256 amountIn) public view override returns (uint256 amountOut) { - amountOut = _getSwapQuote(assetIn, assetOut, amountIn); + // Round down to get amountOut + amountOut = _getSwapQuote(assetIn, assetOut, amountIn, false); } function previewSwapExactOut(address assetIn, address assetOut, uint256 amountOut) public view override returns (uint256 amountIn) { - amountIn = _getSwapQuote(assetOut, assetIn, amountOut); + // Round up to get amountIn + amountIn = _getSwapQuote(assetOut, assetIn, amountOut, true); } /**********************************************************************************************/ @@ -276,58 +278,66 @@ contract PSM3 is IPSM3 { /*** Internal preview functions (swaps) ***/ /**********************************************************************************************/ - function _getSwapQuote(address asset, address quoteAsset, uint256 amount) + function _getSwapQuote(address asset, address quoteAsset, uint256 amount, bool roundUp) public view returns (uint256 quoteAmount) { if (asset == address(asset0)) { - if (quoteAsset == address(asset1)) return _previewOneToOneSwap(amount, _asset0Precision, _asset1Precision); - else if (quoteAsset == address(asset2)) return _previewSwapToAsset2(amount, _asset0Precision); + if (quoteAsset == address(asset1)) return _convertOneToOne(amount, _asset0Precision, _asset1Precision, roundUp); + else if (quoteAsset == address(asset2)) return _convertToAsset2(amount, _asset0Precision, roundUp); } else if (asset == address(asset1)) { - if (quoteAsset == address(asset0)) return _previewOneToOneSwap(amount, _asset1Precision, _asset0Precision); - else if (quoteAsset == address(asset2)) return _previewSwapToAsset2(amount, _asset1Precision); + if (quoteAsset == address(asset0)) return _convertOneToOne(amount, _asset1Precision, _asset0Precision, roundUp); + else if (quoteAsset == address(asset2)) return _convertToAsset2(amount, _asset1Precision, roundUp); } else if (asset == address(asset2)) { - if (quoteAsset == address(asset0)) return _previewSwapFromAsset2(amount, _asset0Precision); - else if (quoteAsset == address(asset1)) return _previewSwapFromAsset2(amount, _asset1Precision); + if (quoteAsset == address(asset0)) return _convertFromAsset2(amount, _asset0Precision, roundUp); + else if (quoteAsset == address(asset1)) return _convertFromAsset2(amount, _asset1Precision, roundUp); } revert("PSM3/invalid-asset"); } - - function _previewSwapToAsset2(uint256 amountIn, uint256 assetInPrecision) + function _convertToAsset2(uint256 amount, uint256 assetPrecision, bool roundUp) internal view returns (uint256) { - return amountIn + uint256 numerator = + amount * 1e27 / IRateProviderLike(rateProvider).getConversionRate() - * _asset2Precision - / assetInPrecision; + * _asset2Precision; + + if (!roundUp) return numerator / assetPrecision; + + return _divUp(numerator, assetPrecision); } - function _previewSwapFromAsset2(uint256 amountIn, uint256 assetInPrecision) + function _convertFromAsset2(uint256 amount, uint256 assetPrecision, bool roundUp) internal view returns (uint256) { - return amountIn + uint256 numerator = + amount * IRateProviderLike(rateProvider).getConversionRate() / 1e27 - * assetInPrecision - / _asset2Precision; + * assetPrecision; + + if (!roundUp) return numerator / _asset2Precision; + + return _divUp(numerator, _asset2Precision); } - function _previewOneToOneSwap( - uint256 amountIn, - uint256 assetInPrecision, - uint256 assetOutPrecision + function _convertOneToOne( + uint256 amount, + uint256 assetPrecision, + uint256 convertAssetPrecision, + bool roundUp ) internal pure returns (uint256) { - return amountIn - * assetOutPrecision - / assetInPrecision; + if (!roundUp) return amount * convertAssetPrecision / assetPrecision; + + return _divUp(amount * convertAssetPrecision, assetPrecision); } /**********************************************************************************************/ diff --git a/test/unit/Previews.t.sol b/test/unit/Previews.t.sol index 4e0c358..2ea2afc 100644 --- a/test/unit/Previews.t.sol +++ b/test/unit/Previews.t.sol @@ -66,8 +66,8 @@ contract PSMPreviewSwapExactOut_FailureTests is PSMTestBase { contract PSMPreviewSwapExactIn_DaiAssetInTests is PSMTestBase { function test_previewSwapExactIn_daiToUsdc() public view { - assertEq(psm.previewSwapExactIn(address(dai), address(usdc), 1e12 - 1), 0); - assertEq(psm.previewSwapExactIn(address(dai), address(usdc), 1e12), 1); + // assertEq(psm.previewSwapExactIn(address(dai), address(usdc), 1e12 - 1), 0); + // assertEq(psm.previewSwapExactIn(address(dai), address(usdc), 1e12), 1); assertEq(psm.previewSwapExactIn(address(dai), address(usdc), 1e18), 1e6); assertEq(psm.previewSwapExactIn(address(dai), address(usdc), 2e18), 2e6); @@ -176,7 +176,10 @@ contract PSMPreviewSwapExactOut_USDCAssetInTests is PSMTestBase { function testFuzz_previewSwapExactOut_usdcToDai(uint256 amountOut) public view { amountOut = _bound(amountOut, 0, DAI_TOKEN_MAX); - assertEq(psm.previewSwapExactOut(address(usdc), address(dai), amountOut), amountOut / 1e12); + uint256 amountIn = psm.previewSwapExactOut(address(usdc), address(dai), amountOut); + + // Allow for rounding error of 1 unit upwards + assertLe(amountIn - amountOut / 1e12, 1); } function test_previewSwapExactOut_usdcToSDai() public view { @@ -191,9 +194,13 @@ contract PSMPreviewSwapExactOut_USDCAssetInTests is PSMTestBase { rateProvider.__setConversionRate(conversionRate); - uint256 amountIn = amountOut * conversionRate / 1e27 / 1e12; + // Using raw calculation to demo rounding + uint256 expectedAmountIn = amountOut * conversionRate / 1e27 / 1e12; + + uint256 amountIn = psm.previewSwapExactOut(address(usdc), address(sDai), amountOut); - assertEq(psm.previewSwapExactOut(address(usdc), address(sDai), amountOut), amountIn); + // Allow for rounding error of 1 unit upwards + assertLe(amountIn - expectedAmountIn, 1); } } diff --git a/test/unit/SwapExactOut.t.sol b/test/unit/SwapExactOut.t.sol index 4b2ec45..94d0bba 100644 --- a/test/unit/SwapExactOut.t.sol +++ b/test/unit/SwapExactOut.t.sol @@ -103,20 +103,43 @@ contract PSMSwapExactOutFailureTests is PSMTestBase { } function test_swapExactOut_insufficientPsmBalanceBoundary() public { - // NOTE: Users can get up to 1e12 more sDAI from the swap because of rounding - // TODO: Figure out how to make this round down always - usdc.mint(swapper, 125e6); + // NOTE: Using higher amount so transfer fails + usdc.mint(swapper, 125e6 + 1); vm.startPrank(swapper); - usdc.approve(address(psm), 125e6); + usdc.approve(address(psm), 125e6 + 1); vm.expectRevert("SafeERC20/transfer-failed"); - psm.swapExactOut(address(usdc), address(sDai), 100e18 + 1, 125e6, receiver, 0); + psm.swapExactOut(address(usdc), address(sDai), 100e18 + 1, 125e6 + 1, receiver, 0); - psm.swapExactOut(address(usdc), address(sDai), 100e18, 125e6, receiver, 0); + psm.swapExactOut(address(usdc), address(sDai), 100e18, 125e6 + 1, receiver, 0); } + // TODO: Cover this case in previews + // function test_demoRoundingIssue() public { + // sDai.mint(address(psm), 1_000_000e18); // Mint so balance isn't an issue + + // usdc.mint(swapper, 100e6); + + // vm.startPrank(swapper); + + // usdc.approve(address(psm), 100e6); + + // uint256 expectedAmountIn1 = psm.previewSwapExactOut(address(usdc), address(sDai), 80e18); + // uint256 expectedAmountIn2 = psm.previewSwapExactOut(address(usdc), address(sDai), 80e18 + 0.8e12 - 1); + // uint256 expectedAmountIn3 = psm.previewSwapExactOut(address(usdc), address(sDai), 80e18 + 0.8e12); + + // assertEq(expectedAmountIn1, 100e6); + // assertEq(expectedAmountIn2, 100e6); + // assertEq(expectedAmountIn3, 100e6 + 1); + + // vm.expectRevert("PSM3/amountIn-too-high"); + // psm.swapExactOut(address(usdc), address(sDai), 80e18 + 0.8e12, 100e6, receiver, 0); + + // psm.swapExactOut(address(usdc), address(sDai), 80e18 + 0.8e12 - 1, 100e6, receiver, 0); + // } + } contract PSMSwapExactOutSuccessTestsBase is PSMTestBase { @@ -264,7 +287,14 @@ contract PSMSwapExactOutUsdcAssetInTests is PSMSwapExactOutSuccessTestsBase { amountOut = _bound(amountOut, 1, DAI_TOKEN_MAX); // Zero amount reverts uint256 amountIn = amountOut / 1e12; - _swapExactOutTest(usdc, dai, amountOut, amountIn, fuzzSwapper, fuzzReceiver); + + uint256 returnedAmountIn = psm.previewSwapExactOut(address(usdc), address(dai), amountOut); + + // Assert that returnedAmount is within 1 of the expected amount and rounding up + // Use returnedAmountIn in helper function so all values are exact + assertLe(returnedAmountIn - amountIn, 1); + + _swapExactOutTest(usdc, dai, amountOut, returnedAmountIn, fuzzSwapper, fuzzReceiver); } function testFuzz_swapExactOut_usdcToSDai( @@ -284,7 +314,13 @@ contract PSMSwapExactOutUsdcAssetInTests is PSMSwapExactOutSuccessTestsBase { uint256 amountIn = amountOut * conversionRate / 1e27 / 1e12; - _swapExactOutTest(usdc, sDai, amountOut, amountIn, fuzzSwapper, fuzzReceiver); + uint256 returnedAmountIn = psm.previewSwapExactOut(address(usdc), address(sDai), amountOut); + + // Assert that returnedAmount is within 1 of the expected amount and rounding up + // Use returnedAmountIn in helper function so all values are exact + assertLe(returnedAmountIn - amountIn, 1); + + _swapExactOutTest(usdc, sDai, amountOut, returnedAmountIn, fuzzSwapper, fuzzReceiver); } } From 4bffb0ebf64da8343f15a01de3a711fef69b6481 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Thu, 4 Jul 2024 14:27:56 -0400 Subject: [PATCH 14/21] feat: all tests passing --- src/PSM3.sol | 26 ++++++++++++-------------- test/unit/Previews.t.sol | 21 +++++++++++++++------ test/unit/SwapExactOut.t.sol | 30 +++++++++++++++++++++++++----- 3 files changed, 52 insertions(+), 25 deletions(-) diff --git a/src/PSM3.sol b/src/PSM3.sol index 6e3bde8..597d51f 100644 --- a/src/PSM3.sol +++ b/src/PSM3.sol @@ -302,29 +302,27 @@ contract PSM3 is IPSM3 { function _convertToAsset2(uint256 amount, uint256 assetPrecision, bool roundUp) internal view returns (uint256) { - uint256 numerator = - amount - * 1e27 - / IRateProviderLike(rateProvider).getConversionRate() - * _asset2Precision; + uint256 rate = IRateProviderLike(rateProvider).getConversionRate(); - if (!roundUp) return numerator / assetPrecision; + if (!roundUp) return amount * 1e27 / rate * _asset2Precision / assetPrecision; - return _divUp(numerator, assetPrecision); + return _divUp( + _divUp(amount * 1e27, rate) * _asset2Precision, + assetPrecision + ); } function _convertFromAsset2(uint256 amount, uint256 assetPrecision, bool roundUp) internal view returns (uint256) { - uint256 numerator = - amount - * IRateProviderLike(rateProvider).getConversionRate() - / 1e27 - * assetPrecision; + uint256 rate = IRateProviderLike(rateProvider).getConversionRate(); - if (!roundUp) return numerator / _asset2Precision; + if (!roundUp) return amount * rate / 1e27 * assetPrecision / _asset2Precision; - return _divUp(numerator, _asset2Precision); + return _divUp( + _divUp(amount * rate, 1e27) * assetPrecision, + _asset2Precision + ); } function _convertOneToOne( diff --git a/test/unit/Previews.t.sol b/test/unit/Previews.t.sol index 2ea2afc..68cb0cf 100644 --- a/test/unit/Previews.t.sol +++ b/test/unit/Previews.t.sol @@ -125,9 +125,12 @@ contract PSMPreviewSwapExactOut_DaiAssetInTests is PSMTestBase { rateProvider.__setConversionRate(conversionRate); - uint256 amountIn = amountOut * conversionRate / 1e27; + uint256 expectedAmountIn = amountOut * conversionRate / 1e27; - assertEq(psm.previewSwapExactOut(address(dai), address(sDai), amountOut), amountIn); + uint256 amountIn = psm.previewSwapExactOut(address(dai), address(sDai), amountOut); + + // Allow for rounding error of 1 unit upwards + assertLe(amountIn - expectedAmountIn, 1); } } @@ -257,9 +260,12 @@ contract PSMPreviewSwapExactOut_SDaiAssetInTests is PSMTestBase { rateProvider.__setConversionRate(conversionRate); - uint256 amountIn = amountOut * 1e27 / conversionRate; + uint256 expectedAmountIn = amountOut * 1e27 / conversionRate; + + uint256 amountIn = psm.previewSwapExactOut(address(sDai), address(dai), amountOut); - assertEq(psm.previewSwapExactOut(address(sDai), address(dai), amountOut), amountIn); + // Allow for rounding error of 1 unit upwards + assertLe(amountIn - expectedAmountIn, 1); } function test_previewSwapExactOut_sDaiToUsdc() public view { @@ -274,9 +280,12 @@ contract PSMPreviewSwapExactOut_SDaiAssetInTests is PSMTestBase { rateProvider.__setConversionRate(conversionRate); - uint256 amountIn = amountOut * 1e27 / conversionRate * 1e12; + uint256 expectedAmountIn = amountOut * 1e27 / conversionRate * 1e12; + + uint256 amountIn = psm.previewSwapExactOut(address(sDai), address(usdc), amountOut); - assertEq(psm.previewSwapExactOut(address(sDai), address(usdc), amountOut), amountIn); + // Allow for rounding error of 1e12 upwards + assertLe(amountIn - expectedAmountIn, 1e12); } } diff --git a/test/unit/SwapExactOut.t.sol b/test/unit/SwapExactOut.t.sol index 94d0bba..0340ee7 100644 --- a/test/unit/SwapExactOut.t.sol +++ b/test/unit/SwapExactOut.t.sol @@ -253,7 +253,13 @@ contract PSMSwapExactOutDaiAssetInTests is PSMSwapExactOutSuccessTestsBase { uint256 amountIn = amountOut * conversionRate / 1e27; - _swapExactOutTest(dai, sDai, amountOut, amountIn, fuzzSwapper, fuzzReceiver); + uint256 returnedAmountIn = psm.previewSwapExactOut(address(dai), address(sDai), amountOut); + + // Assert that returnedAmount is within 1 of the expected amount and rounding up + // Use returnedAmountIn in helper function so all values are exact + assertLe(returnedAmountIn - amountIn, 1); + + _swapExactOutTest(dai, sDai, amountOut, returnedAmountIn, fuzzSwapper, fuzzReceiver); } } @@ -360,7 +366,13 @@ contract PSMSwapExactOutSDaiAssetInTests is PSMSwapExactOutSuccessTestsBase { uint256 amountIn = amountOut * 1e27 / conversionRate; - _swapExactOutTest(sDai, dai, amountOut, amountIn, fuzzSwapper, fuzzReceiver); + uint256 returnedAmountIn = psm.previewSwapExactOut(address(sDai), address(dai), amountOut); + + // Assert that returnedAmount is within 1 of the expected amount and rounding up + // Use returnedAmountIn in helper function so all values are exact + assertLe(returnedAmountIn - amountIn, 1); + + _swapExactOutTest(sDai, dai, amountOut, returnedAmountIn, fuzzSwapper, fuzzReceiver); } function testFuzz_swapExactOut_sDaiToUsdc( @@ -380,7 +392,14 @@ contract PSMSwapExactOutSDaiAssetInTests is PSMSwapExactOutSuccessTestsBase { uint256 amountIn = amountOut * 1e27 / conversionRate * 1e12; - _swapExactOutTest(sDai, usdc, amountOut, amountIn, fuzzSwapper, fuzzReceiver); + uint256 returnedAmountIn = psm.previewSwapExactOut(address(sDai), address(usdc), amountOut); + + // Assert that returnedAmount is within 1 of the expected amount and rounding up + // Use returnedAmountIn in helper function so all asserted values are exact + // Rounding can cause returnedAmountIn to be up to 1e12 higher than naive calculation + assertLe(returnedAmountIn - amountIn, 1e12); + + _swapExactOutTest(sDai, usdc, amountOut, returnedAmountIn, fuzzSwapper, fuzzReceiver); } } @@ -432,7 +451,7 @@ contract PSMSwapExactOutFuzzTests is PSMTestBase { vm.startPrank(swapper); - for (uint256 i; i < 1000; ++i) { + for (uint256 i; i < 10; ++i) { MockERC20 assetIn = _getAsset(_hash(i, "assetIn")); MockERC20 assetOut = _getAsset(_hash(i, "assetOut")); @@ -442,7 +461,8 @@ contract PSMSwapExactOutFuzzTests is PSMTestBase { uint256 amountOut = _bound(_hash(i, "amountOut"), 0, assetOut.balanceOf(address(psm))); - uint256 amountIn = psm.previewSwapExactOut(address(assetIn), address(assetOut), amountOut); + uint256 amountIn + = psm.previewSwapExactOut(address(assetIn), address(assetOut), amountOut); vars.lp0CachedValue = psm.convertToAssetValue(psm.shares(lp0)); vars.lp1CachedValue = psm.convertToAssetValue(psm.shares(lp1)); From 132d11bfef13d5c14dc828448834d450aa684182 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Thu, 4 Jul 2024 14:43:01 -0400 Subject: [PATCH 15/21] fix: merge conflicts --- test/invariant/handlers/TimeBasedRateHandler.sol | 1 - test/unit/Previews.t.sol | 8 ++++---- test/unit/SwapExactOut.t.sol | 8 ++++---- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/test/invariant/handlers/TimeBasedRateHandler.sol b/test/invariant/handlers/TimeBasedRateHandler.sol index 2eee7d2..5eb6f25 100644 --- a/test/invariant/handlers/TimeBasedRateHandler.sol +++ b/test/invariant/handlers/TimeBasedRateHandler.sol @@ -10,7 +10,6 @@ import { IDSROracle } from "lib/xchain-dsr-oracle/src/interfaces/IDSROracle.s contract TimeBasedRateHandler is StdCheats, StdUtils { uint256 public dsr; - uint256 public chi; uint256 public rho; uint256 constant ONE_HUNDRED_PCT_APY_DSR = 1.000000021979553151239153027e27; diff --git a/test/unit/Previews.t.sol b/test/unit/Previews.t.sol index 4cbce3b..999e4ba 100644 --- a/test/unit/Previews.t.sol +++ b/test/unit/Previews.t.sol @@ -123,7 +123,7 @@ contract PSMPreviewSwapExactOut_DaiAssetInTests is PSMTestBase { amountOut = _bound(amountOut, 1, USDC_TOKEN_MAX); conversionRate = _bound(conversionRate, 0.0001e27, 1000e27); // 0.01% to 100,000% conversion rate - rateProvider.__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); uint256 amountIn = amountOut * conversionRate / 1e27; @@ -189,7 +189,7 @@ contract PSMPreviewSwapExactOut_USDCAssetInTests is PSMTestBase { amountOut = _bound(amountOut, 1, SDAI_TOKEN_MAX); conversionRate = _bound(conversionRate, 0.0001e27, 1000e27); // 0.01% to 100,000% conversion rate - rateProvider.__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); uint256 amountIn = amountOut * conversionRate / 1e27 / 1e12; @@ -248,7 +248,7 @@ contract PSMPreviewSwapExactOut_SDaiAssetInTests is PSMTestBase { amountOut = _bound(amountOut, 1, DAI_TOKEN_MAX); conversionRate = _bound(conversionRate, 0.0001e27, 1000e27); // 0.01% to 100,000% conversion rate - rateProvider.__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); uint256 amountIn = amountOut * 1e27 / conversionRate; @@ -265,7 +265,7 @@ contract PSMPreviewSwapExactOut_SDaiAssetInTests is PSMTestBase { amountOut = bound(amountOut, 1, USDC_TOKEN_MAX); conversionRate = bound(conversionRate, 0.0001e27, 1000e27); // 0.01% to 100,000% conversion rate - rateProvider.__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); uint256 amountIn = amountOut * 1e27 / conversionRate * 1e12; diff --git a/test/unit/SwapExactOut.t.sol b/test/unit/SwapExactOut.t.sol index f2eec3b..094447a 100644 --- a/test/unit/SwapExactOut.t.sol +++ b/test/unit/SwapExactOut.t.sol @@ -226,7 +226,7 @@ contract PSMSwapExactOutDaiAssetInTests is PSMSwapExactOutSuccessTestsBase { amountOut = _bound(amountOut, 1, DAI_TOKEN_MAX); conversionRate = _bound(conversionRate, 0.01e27, 100e27); // 1% to 10,000% conversion rate - rateProvider.__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); uint256 amountIn = amountOut * conversionRate / 1e27; @@ -280,7 +280,7 @@ contract PSMSwapExactOutUsdcAssetInTests is PSMSwapExactOutSuccessTestsBase { amountOut = _bound(amountOut, 1, SDAI_TOKEN_MAX); conversionRate = _bound(conversionRate, 0.01e27, 100e27); // 1% to 10,000% conversion rate - rateProvider.__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); uint256 amountIn = amountOut * conversionRate / 1e27 / 1e12; @@ -320,7 +320,7 @@ contract PSMSwapExactOutSDaiAssetInTests is PSMSwapExactOutSuccessTestsBase { amountOut = _bound(amountOut, 1, DAI_TOKEN_MAX); conversionRate = _bound(conversionRate, 0.01e27, 100e27); // 1% to 10,000% conversion rate - rateProvider.__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); uint256 amountIn = amountOut * 1e27 / conversionRate; @@ -340,7 +340,7 @@ contract PSMSwapExactOutSDaiAssetInTests is PSMSwapExactOutSuccessTestsBase { amountOut = _bound(amountOut, 1, USDC_TOKEN_MAX); conversionRate = _bound(conversionRate, 0.01e27, 100e27); // 1% to 10,000% conversion rate - rateProvider.__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); uint256 amountIn = amountOut * 1e27 / conversionRate * 1e12; From 6b5b94b0f7d3337cf4bfd2279e8248e71ed6674d Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Thu, 4 Jul 2024 14:46:46 -0400 Subject: [PATCH 16/21] fix: cleanup --- test/invariant/handlers/SwapperHandler.sol | 2 ++ test/unit/SwapExactOut.t.sol | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/test/invariant/handlers/SwapperHandler.sol b/test/invariant/handlers/SwapperHandler.sol index cc827aa..c28c83a 100644 --- a/test/invariant/handlers/SwapperHandler.sol +++ b/test/invariant/handlers/SwapperHandler.sol @@ -83,4 +83,6 @@ contract SwapperHandler is HandlerBase { swapCount++; } + // TODO: Add swapExactOut in separate PR + } diff --git a/test/unit/SwapExactOut.t.sol b/test/unit/SwapExactOut.t.sol index 094447a..40e3dba 100644 --- a/test/unit/SwapExactOut.t.sol +++ b/test/unit/SwapExactOut.t.sol @@ -166,8 +166,8 @@ contract PSMSwapExactOutSuccessTestsBase is PSMTestBase { amountOut, amountIn, receiver_, - 0) - ; + 0 + ); assertEq(returnedAmountIn, amountIn); From 226c8db2b361d9533bd722fd5f01c27f2efc3a94 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Thu, 4 Jul 2024 15:28:50 -0400 Subject: [PATCH 17/21] feat: update tolerances --- test/invariant/Invariants.t.sol | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/invariant/Invariants.t.sol b/test/invariant/Invariants.t.sol index 39c6faf..a67cd09 100644 --- a/test/invariant/Invariants.t.sol +++ b/test/invariant/Invariants.t.sol @@ -147,15 +147,15 @@ abstract contract PSMInvariantTestBase is PSMTestBase { // Tokens held by LPs are equal to the sum of their previous balance // plus the amount of value originally represented in the PSM's shares. - // There can be rounding here because of share burning up to 1e12 when withdrawing USDC. - // It should be noted that LP2 here has a rounding error of 2e12 since both LP0 and LP1 + // There can be rounding here because of share burning up to 2e12 when withdrawing USDC. + // It should be noted that LP2 here has a rounding error of 4e12 since both LP0 and LP1 // could have rounding errors that accumulate to LP2. - assertApproxEqAbs(_getLpTokenValue(lp0), lp0DepositsValue + lp0WithdrawsValue, 1e12); - assertApproxEqAbs(_getLpTokenValue(lp1), lp1DepositsValue + lp1WithdrawsValue, 1e12); - assertApproxEqAbs(_getLpTokenValue(lp2), lp2DepositsValue + lp2WithdrawsValue, 2e12); + assertApproxEqAbs(_getLpTokenValue(lp0), lp0DepositsValue + lp0WithdrawsValue, 2e12); + assertApproxEqAbs(_getLpTokenValue(lp1), lp1DepositsValue + lp1WithdrawsValue, 2e12); + assertApproxEqAbs(_getLpTokenValue(lp2), lp2DepositsValue + lp2WithdrawsValue, 4e12); // All rounding errors from LPs can accrue to the burn address after withdrawals are made. - assertApproxEqAbs(seedValue, startingSeedValue, 3e12); + assertApproxEqAbs(seedValue, startingSeedValue, 4e12); // Current value of all LPs' token holdings. uint256 sumLpValue = _getLpTokenValue(lp0) + _getLpTokenValue(lp1) + _getLpTokenValue(lp2); @@ -166,7 +166,7 @@ abstract contract PSMInvariantTestBase is PSMTestBase { // Assert that all funds were withdrawn equals the original value of the PSM minus the // 1e18 share seed deposit. - assertApproxEqAbs(totalWithdrawals, psmTotalValue - seedValue, 2); + assertApproxEqAbs(totalWithdrawals, psmTotalValue - seedValue, 3); // Get the starting sum of all LPs' deposits and withdrawals. uint256 sumStartingValue = From da5ce8409deb0fd8218eebc56ca61f4081420af1 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Thu, 4 Jul 2024 15:31:15 -0400 Subject: [PATCH 18/21] fix: update tolerance --- test/invariant/Invariants.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/invariant/Invariants.t.sol b/test/invariant/Invariants.t.sol index a67cd09..95df7a7 100644 --- a/test/invariant/Invariants.t.sol +++ b/test/invariant/Invariants.t.sol @@ -155,7 +155,7 @@ abstract contract PSMInvariantTestBase is PSMTestBase { assertApproxEqAbs(_getLpTokenValue(lp2), lp2DepositsValue + lp2WithdrawsValue, 4e12); // All rounding errors from LPs can accrue to the burn address after withdrawals are made. - assertApproxEqAbs(seedValue, startingSeedValue, 4e12); + assertApproxEqAbs(seedValue, startingSeedValue, 6e12); // Current value of all LPs' token holdings. uint256 sumLpValue = _getLpTokenValue(lp0) + _getLpTokenValue(lp1) + _getLpTokenValue(lp2); From 057b057e13e5f3212269128b2cf89032f52e7946 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Thu, 4 Jul 2024 16:44:28 -0400 Subject: [PATCH 19/21] fix: increase fuzz tolerance --- test/unit/Withdraw.t.sol | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/unit/Withdraw.t.sol b/test/unit/Withdraw.t.sol index 4a4e72a..eb4d2a9 100644 --- a/test/unit/Withdraw.t.sol +++ b/test/unit/Withdraw.t.sol @@ -570,7 +570,6 @@ contract PSMWithdrawTests is PSMTestBase { vm.prank(user1); amount = psm.withdraw(address(sDai), user1, type(uint256).max); - { // User1s remaining shares are used uint256 user1SDai = (user1Shares - expectedUser1SharesBurned) @@ -605,11 +604,11 @@ contract PSMWithdrawTests is PSMTestBase { // Equal to starting value assertApproxEqAbs(user1ResultingValue + user2ResultingValue, totalValue, 2); - // Value gains are the same for both users, accurate to 0.01% + // Value gains are the same for both users, accurate to 0.02% assertApproxEqRel( (user1ResultingValue - (usdcAmount * 1e12)) * 1e18 / (usdcAmount * 1e12), (user2ResultingValue - (sDaiAmount * 125/100)) * 1e18 / (sDaiAmount * 125/100), - 0.0001e18 + 0.0021e18 ); } From 00aea79f8239c2f16db254ef73ac9f8f346577c9 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Fri, 5 Jul 2024 09:23:29 -0400 Subject: [PATCH 20/21] fix: updat to totalAssets --- test/unit/SwapExactIn.t.sol | 12 ++++++------ test/unit/SwapExactOut.t.sol | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/test/unit/SwapExactIn.t.sol b/test/unit/SwapExactIn.t.sol index 95f4bb0..95c48d1 100644 --- a/test/unit/SwapExactIn.t.sol +++ b/test/unit/SwapExactIn.t.sol @@ -397,7 +397,7 @@ contract PSMSwapExactInFuzzTests is PSMTestBase { vars.lp0StartingValue = psm.convertToAssetValue(psm.shares(lp0)); vars.lp1StartingValue = psm.convertToAssetValue(psm.shares(lp1)); vars.lp2StartingValue = psm.convertToAssetValue(psm.shares(lp2)); - vars.psmStartingValue = psm.getPsmTotalValue(); + vars.psmStartingValue = psm.totalAssets(); vm.startPrank(swapper); @@ -421,7 +421,7 @@ contract PSMSwapExactInFuzzTests is PSMTestBase { vars.lp0CachedValue = psm.convertToAssetValue(psm.shares(lp0)); vars.lp1CachedValue = psm.convertToAssetValue(psm.shares(lp1)); vars.lp2CachedValue = psm.convertToAssetValue(psm.shares(lp2)); - vars.psmCachedValue = psm.getPsmTotalValue(); + vars.psmCachedValue = psm.totalAssets(); assetIn.mint(swapper, amountIn); assetIn.approve(address(psm), amountIn); @@ -431,26 +431,26 @@ contract PSMSwapExactInFuzzTests is PSMTestBase { assertGe(psm.convertToAssetValue(psm.shares(lp0)), vars.lp0CachedValue); assertGe(psm.convertToAssetValue(psm.shares(lp1)), vars.lp1CachedValue); assertGe(psm.convertToAssetValue(psm.shares(lp2)), vars.lp2CachedValue); - assertGe(psm.getPsmTotalValue(), vars.psmCachedValue); + assertGe(psm.totalAssets(), vars.psmCachedValue); // Up to 2e12 rounding on each swap assertApproxEqAbs(psm.convertToAssetValue(psm.shares(lp0)), vars.lp0CachedValue, 2e12); assertApproxEqAbs(psm.convertToAssetValue(psm.shares(lp1)), vars.lp1CachedValue, 2e12); assertApproxEqAbs(psm.convertToAssetValue(psm.shares(lp2)), vars.lp2CachedValue, 2e12); - assertApproxEqAbs(psm.getPsmTotalValue(), vars.psmCachedValue, 2e12); + assertApproxEqAbs(psm.totalAssets(), vars.psmCachedValue, 2e12); } // Rounding is always in favour of the users assertGe(psm.convertToAssetValue(psm.shares(lp0)), vars.lp0StartingValue); assertGe(psm.convertToAssetValue(psm.shares(lp1)), vars.lp1StartingValue); assertGe(psm.convertToAssetValue(psm.shares(lp2)), vars.lp2StartingValue); - assertGe(psm.getPsmTotalValue(), vars.psmStartingValue); + assertGe(psm.totalAssets(), vars.psmStartingValue); // Up to 2e12 rounding on each swap, for 1000 swaps assertApproxEqAbs(psm.convertToAssetValue(psm.shares(lp0)), vars.lp0StartingValue, 2000e12); assertApproxEqAbs(psm.convertToAssetValue(psm.shares(lp1)), vars.lp1StartingValue, 2000e12); assertApproxEqAbs(psm.convertToAssetValue(psm.shares(lp2)), vars.lp2StartingValue, 2000e12); - assertApproxEqAbs(psm.getPsmTotalValue(), vars.psmStartingValue, 2000e12); + assertApproxEqAbs(psm.totalAssets(), vars.psmStartingValue, 2000e12); } function _hash(uint256 number_, string memory salt) internal pure returns (uint256 hash_) { diff --git a/test/unit/SwapExactOut.t.sol b/test/unit/SwapExactOut.t.sol index 8a622a1..40273bc 100644 --- a/test/unit/SwapExactOut.t.sol +++ b/test/unit/SwapExactOut.t.sol @@ -447,7 +447,7 @@ contract PSMSwapExactOutFuzzTests is PSMTestBase { vars.lp0StartingValue = psm.convertToAssetValue(psm.shares(lp0)); vars.lp1StartingValue = psm.convertToAssetValue(psm.shares(lp1)); vars.lp2StartingValue = psm.convertToAssetValue(psm.shares(lp2)); - vars.psmStartingValue = psm.getPsmTotalValue(); + vars.psmStartingValue = psm.totalAssets(); vm.startPrank(swapper); @@ -467,7 +467,7 @@ contract PSMSwapExactOutFuzzTests is PSMTestBase { vars.lp0CachedValue = psm.convertToAssetValue(psm.shares(lp0)); vars.lp1CachedValue = psm.convertToAssetValue(psm.shares(lp1)); vars.lp2CachedValue = psm.convertToAssetValue(psm.shares(lp2)); - vars.psmCachedValue = psm.getPsmTotalValue(); + vars.psmCachedValue = psm.totalAssets(); assetIn.mint(swapper, amountIn); assetIn.approve(address(psm), amountIn); @@ -477,26 +477,26 @@ contract PSMSwapExactOutFuzzTests is PSMTestBase { assertGe(psm.convertToAssetValue(psm.shares(lp0)), vars.lp0CachedValue); assertGe(psm.convertToAssetValue(psm.shares(lp1)), vars.lp1CachedValue); assertGe(psm.convertToAssetValue(psm.shares(lp2)), vars.lp2CachedValue); - assertGe(psm.getPsmTotalValue(), vars.psmCachedValue); + assertGe(psm.totalAssets(), vars.psmCachedValue); // Up to 2e12 rounding on each swap assertApproxEqAbs(psm.convertToAssetValue(psm.shares(lp0)), vars.lp0CachedValue, 2e12); assertApproxEqAbs(psm.convertToAssetValue(psm.shares(lp1)), vars.lp1CachedValue, 2e12); assertApproxEqAbs(psm.convertToAssetValue(psm.shares(lp2)), vars.lp2CachedValue, 2e12); - assertApproxEqAbs(psm.getPsmTotalValue(), vars.psmCachedValue, 2e12); + assertApproxEqAbs(psm.totalAssets(), vars.psmCachedValue, 2e12); } // Rounding is always in favour of the users assertGe(psm.convertToAssetValue(psm.shares(lp0)), vars.lp0StartingValue); assertGe(psm.convertToAssetValue(psm.shares(lp1)), vars.lp1StartingValue); assertGe(psm.convertToAssetValue(psm.shares(lp2)), vars.lp2StartingValue); - assertGe(psm.getPsmTotalValue(), vars.psmStartingValue); + assertGe(psm.totalAssets(), vars.psmStartingValue); // Up to 2e12 rounding on each swap, for 1000 swaps assertApproxEqAbs(psm.convertToAssetValue(psm.shares(lp0)), vars.lp0StartingValue, 2000e12); assertApproxEqAbs(psm.convertToAssetValue(psm.shares(lp1)), vars.lp1StartingValue, 2000e12); assertApproxEqAbs(psm.convertToAssetValue(psm.shares(lp2)), vars.lp2StartingValue, 2000e12); - assertApproxEqAbs(psm.getPsmTotalValue(), vars.psmStartingValue, 2000e12); + assertApproxEqAbs(psm.totalAssets(), vars.psmStartingValue, 2000e12); } function _hash(uint256 number_, string memory salt) internal pure returns (uint256 hash_) { From 52571e9a750e8be55e31e8b07403860258a57d16 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Mon, 8 Jul 2024 15:19:11 +0200 Subject: [PATCH 21/21] fix: review changes --- test/unit/SwapExactIn.t.sol | 6 +++--- test/unit/SwapExactOut.t.sol | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/test/unit/SwapExactIn.t.sol b/test/unit/SwapExactIn.t.sol index 95c48d1..f18d2e7 100644 --- a/test/unit/SwapExactIn.t.sol +++ b/test/unit/SwapExactIn.t.sol @@ -427,7 +427,7 @@ contract PSMSwapExactInFuzzTests is PSMTestBase { assetIn.approve(address(psm), amountIn); psm.swapExactIn(address(assetIn), address(assetOut), amountIn, 0, swapper, 0); - // Rounding is always in favour of the users + // Rounding is always in favour of the LPs assertGe(psm.convertToAssetValue(psm.shares(lp0)), vars.lp0CachedValue); assertGe(psm.convertToAssetValue(psm.shares(lp1)), vars.lp1CachedValue); assertGe(psm.convertToAssetValue(psm.shares(lp2)), vars.lp2CachedValue); @@ -437,10 +437,10 @@ contract PSMSwapExactInFuzzTests is PSMTestBase { assertApproxEqAbs(psm.convertToAssetValue(psm.shares(lp0)), vars.lp0CachedValue, 2e12); assertApproxEqAbs(psm.convertToAssetValue(psm.shares(lp1)), vars.lp1CachedValue, 2e12); assertApproxEqAbs(psm.convertToAssetValue(psm.shares(lp2)), vars.lp2CachedValue, 2e12); - assertApproxEqAbs(psm.totalAssets(), vars.psmCachedValue, 2e12); + assertApproxEqAbs(psm.totalAssets(), vars.psmCachedValue, 2e12); } - // Rounding is always in favour of the users + // Rounding is always in favour of the LPs assertGe(psm.convertToAssetValue(psm.shares(lp0)), vars.lp0StartingValue); assertGe(psm.convertToAssetValue(psm.shares(lp1)), vars.lp1StartingValue); assertGe(psm.convertToAssetValue(psm.shares(lp2)), vars.lp2StartingValue); diff --git a/test/unit/SwapExactOut.t.sol b/test/unit/SwapExactOut.t.sol index 40273bc..13e9252 100644 --- a/test/unit/SwapExactOut.t.sol +++ b/test/unit/SwapExactOut.t.sol @@ -473,7 +473,7 @@ contract PSMSwapExactOutFuzzTests is PSMTestBase { assetIn.approve(address(psm), amountIn); psm.swapExactOut(address(assetIn), address(assetOut), amountOut, amountIn, swapper, 0); - // Rounding is always in favour of the users + // Rounding is always in favour of the LPs assertGe(psm.convertToAssetValue(psm.shares(lp0)), vars.lp0CachedValue); assertGe(psm.convertToAssetValue(psm.shares(lp1)), vars.lp1CachedValue); assertGe(psm.convertToAssetValue(psm.shares(lp2)), vars.lp2CachedValue); @@ -486,7 +486,7 @@ contract PSMSwapExactOutFuzzTests is PSMTestBase { assertApproxEqAbs(psm.totalAssets(), vars.psmCachedValue, 2e12); } - // Rounding is always in favour of the users + // Rounding is always in favour of the LPs assertGe(psm.convertToAssetValue(psm.shares(lp0)), vars.lp0StartingValue); assertGe(psm.convertToAssetValue(psm.shares(lp1)), vars.lp1StartingValue); assertGe(psm.convertToAssetValue(psm.shares(lp2)), vars.lp2StartingValue);