From 039ae151de3b7b1f96cf272633046b13b10727b7 Mon Sep 17 00:00:00 2001 From: Lucas Manuel Date: Wed, 17 Jul 2024 11:45:31 +0200 Subject: [PATCH] feat: Add fuzz testing for swap functionality, add rounding up (SC-482) (#22) * feat: update swap to swapExactAmountIn * feat: add preview test coverage * feat: add new function * feat: create new file, failure tests passing * ci: add coverage backgp * feat: get new swap tests passing * feat: set up initial test * feat: refactor code to add returns * feat: add return value assertions * feat: working fuzz test * feat: add swap exact out, show that rounding is against user * fix: update natspec * feat: refactor to round up on swap exact out * feat: all tests passing * fix: merge conflicts * fix: cleanup * feat: update tolerances * fix: update tolerance * fix: increase fuzz tolerance * fix: updat to totalAssets * fix: review changes --- src/PSM3.sol | 66 ++++++------ test/unit/Previews.t.sol | 34 +++++-- test/unit/SwapExactIn.t.sol | 117 +++++++++++++++++++++- test/unit/SwapExactOut.t.sol | 188 +++++++++++++++++++++++++++++++++-- 4 files changed, 355 insertions(+), 50 deletions(-) diff --git a/src/PSM3.sol b/src/PSM3.sol index f4e7f2d..74599e7 100644 --- a/src/PSM3.sol +++ b/src/PSM3.sol @@ -178,13 +178,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); } /**********************************************************************************************/ @@ -271,58 +273,64 @@ 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 - * 1e27 - / IRateProviderLike(rateProvider).getConversionRate() - * _asset2Precision - / assetInPrecision; + uint256 rate = IRateProviderLike(rateProvider).getConversionRate(); + + if (!roundUp) return amount * 1e27 / rate * _asset2Precision / assetPrecision; + + return _divUp( + _divUp(amount * 1e27, rate) * _asset2Precision, + assetPrecision + ); } - function _previewSwapFromAsset2(uint256 amountIn, uint256 assetInPrecision) + function _convertFromAsset2(uint256 amount, uint256 assetPrecision, bool roundUp) internal view returns (uint256) { - return amountIn - * IRateProviderLike(rateProvider).getConversionRate() - / 1e27 - * assetInPrecision - / _asset2Precision; + uint256 rate = IRateProviderLike(rateProvider).getConversionRate(); + + if (!roundUp) return amount * rate / 1e27 * assetPrecision / _asset2Precision; + + return _divUp( + _divUp(amount * rate, 1e27) * assetPrecision, + _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 999e4ba..d03cff2 100644 --- a/test/unit/Previews.t.sol +++ b/test/unit/Previews.t.sol @@ -125,9 +125,12 @@ contract PSMPreviewSwapExactOut_DaiAssetInTests is PSMTestBase { mockRateProvider.__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); } } @@ -176,7 +179,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 +197,13 @@ contract PSMPreviewSwapExactOut_USDCAssetInTests is PSMTestBase { mockRateProvider.__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); } } @@ -250,9 +260,12 @@ contract PSMPreviewSwapExactOut_SDaiAssetInTests is PSMTestBase { mockRateProvider.__setConversionRate(conversionRate); - uint256 amountIn = amountOut * 1e27 / conversionRate; + uint256 expectedAmountIn = amountOut * 1e27 / conversionRate; - assertEq(psm.previewSwapExactOut(address(sDai), address(dai), amountOut), amountIn); + uint256 amountIn = psm.previewSwapExactOut(address(sDai), address(dai), amountOut); + + // Allow for rounding error of 1 unit upwards + assertLe(amountIn - expectedAmountIn, 1); } function test_previewSwapExactOut_sDaiToUsdc() public view { @@ -267,9 +280,12 @@ contract PSMPreviewSwapExactOut_SDaiAssetInTests is PSMTestBase { mockRateProvider.__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/SwapExactIn.t.sol b/test/unit/SwapExactIn.t.sol index 57b83d5..f18d2e7 100644 --- a/test/unit/SwapExactIn.t.sol +++ b/test/unit/SwapExactIn.t.sol @@ -5,7 +5,7 @@ import "forge-std/Test.sol"; import { PSM3 } from "src/PSM3.sol"; -import { MockERC20, MockRateProvider, PSMTestBase } from "test/PSMTestBase.sol"; +import { MockERC20, PSMTestBase } from "test/PSMTestBase.sol"; contract PSMSwapExactInFailureTests is PSMTestBase { @@ -353,3 +353,118 @@ contract PSMSwapExactInSDaiAssetInTests is PSMSwapExactInSuccessTestsBase { } } + +contract PSMSwapExactInFuzzTests 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_swapExactIn( + uint256 conversionRate, + uint256 depositSeed + ) public { + // 1% to 200% conversion rate + mockRateProvider.__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.totalAssets(); + + 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(assetIn), + address(assetOut), + assetOut.balanceOf(address(psm)) + ); + + 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.totalAssets(); + + 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 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); + 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.totalAssets(), vars.psmCachedValue, 2e12); + } + + // 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); + 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.totalAssets(), 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 40e3dba..13e9252 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 { @@ -230,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); } } @@ -264,7 +293,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 +320,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); } } @@ -324,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( @@ -344,7 +392,125 @@ 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); + } + +} + +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 + mockRateProvider.__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.totalAssets(); + + vm.startPrank(swapper); + + for (uint256 i; i < 10; ++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.totalAssets(); + + 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 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); + 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.totalAssets(), vars.psmCachedValue, 2e12); + } + + // 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); + 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.totalAssets(), 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"); } }