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/src/PSM3.sol b/src/PSM3.sol index 1bbd8ec..4eae245 100644 --- a/src/PSM3.sol +++ b/src/PSM3.sol @@ -53,7 +53,7 @@ contract PSM3 is IPSM3 { /*** Swap functions ***/ /**********************************************************************************************/ - function swap( + function swapExactIn( address assetIn, address assetOut, uint256 amountIn, @@ -66,7 +66,7 @@ contract PSM3 is IPSM3 { require(amountIn != 0, "PSM3/invalid-amountIn"); require(receiver != address(0), "PSM3/invalid-receiver"); - amountOut = previewSwap(assetIn, assetOut, amountIn); + amountOut = previewSwapExactIn(assetIn, assetOut, amountIn); require(amountOut >= minAmountOut, "PSM3/amountOut-too-low"); @@ -76,6 +76,29 @@ contract PSM3 is IPSM3 { emit Swap(assetIn, assetOut, msg.sender, receiver, amountIn, amountOut, referralCode); } + function swapExactOut( + address assetIn, + address assetOut, + uint256 amountOut, + uint256 maxAmountIn, + address receiver, + uint256 referralCode + ) + external override returns (uint256 amountIn) + { + require(amountOut != 0, "PSM3/invalid-amountOut"); + require(receiver != address(0), "PSM3/invalid-receiver"); + + amountIn = previewSwapExactOut(assetIn, assetOut, amountOut); + + require(amountIn <= maxAmountIn, "PSM3/amountIn-too-high"); + + 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 ***/ /**********************************************************************************************/ @@ -154,25 +177,18 @@ 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); - } + // Round down to get amountOut + amountOut = _getSwapQuote(assetIn, assetOut, amountIn, false); + } - revert("PSM3/invalid-asset"); + function previewSwapExactOut(address assetIn, address assetOut, uint256 amountOut) + public view override returns (uint256 amountIn) + { + // Round up to get amountIn + amountIn = _getSwapQuote(assetOut, assetIn, amountOut, true); } /**********************************************************************************************/ @@ -229,23 +245,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); @@ -269,40 +271,90 @@ 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, bool roundUp) + public view returns (uint256 quoteAmount) + { + if (asset == address(asset0)) { + 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 _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 _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); + } + + /**********************************************************************************************/ + /*** 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 c8882c3..4f02ca4 100644 --- a/src/interfaces/IPSM3.sol +++ b/src/interfaces/IPSM3.sol @@ -113,19 +113,19 @@ 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. - * @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 Amount of the asset that will be received in 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 swap( + function swapExactIn( address assetIn, address assetOut, uint256 amountIn, @@ -134,6 +134,28 @@ interface IPSM3 { uint256 referralCode ) 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 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 amountOut, + uint256 maxAmountIn, + address receiver, + uint256 referralCode + ) external returns (uint256 amountIn); + /**********************************************************************************************/ /*** Liquidity provision functions ***/ /**********************************************************************************************/ @@ -206,9 +228,21 @@ 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); + /** + * @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); + /**********************************************************************************************/ /*** Conversion functions ***/ /**********************************************************************************************/ diff --git a/test/invariant/handlers/SwapperHandler.sol b/test/invariant/handlers/SwapperHandler.sol index 150b5cc..6e05288 100644 --- a/test/invariant/handlers/SwapperHandler.sol +++ b/test/invariant/handlers/SwapperHandler.sol @@ -47,7 +47,7 @@ contract SwapperHandler is HandlerBase { return swappers[indexSeed % swappers.length]; } - function swap( + function swapExactIn( uint256 assetInSeed, uint256 assetOutSeed, uint256 swapperSeed, @@ -72,7 +72,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)) @@ -90,7 +90,7 @@ contract SwapperHandler is HandlerBase { minAmountOut = _bound( minAmountOut, 0, - psm.previewSwap(address(assetIn), address(assetOut), amountIn) + psm.previewSwapExactIn(address(assetIn), address(assetOut), amountIn) ); // 2. Cache starting state @@ -103,8 +103,14 @@ contract SwapperHandler is HandlerBase { vm.startPrank(swapper); assetIn.mint(swapper, amountIn); assetIn.approve(address(psm), amountIn); - uint256 amountOut - = psm.swap(address(assetIn), address(assetOut), amountIn, minAmountOut, swapper, 0); + uint256 amountOut = psm.swapExactIn( + address(assetIn), + address(assetOut), + amountIn, + minAmountOut, + swapper, + 0 + ); vm.stopPrank(); // 4. Update ghost variable(s) @@ -175,4 +181,6 @@ contract SwapperHandler is HandlerBase { swapCount++; } + // TODO: Add swapExactOut in separate PR + } 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/Previews.t.sol b/test/unit/Previews.t.sol index 2d7e2f0..cf5420e 100644 --- a/test/unit/Previews.t.sol +++ b/test/unit/Previews.t.sol @@ -5,59 +5,88 @@ import "forge-std/Test.sol"; import { MockRateProvider, 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); + } - 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); + function test_previewSwapExactOut_bothAsset0() public { + vm.expectRevert("PSM3/invalid-asset"); + psm.previewSwapExactOut(address(dai), address(dai), 1); } - function testFuzz_previewSwap_daiToUsdc(uint256 amountIn) public view { + 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.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_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,68 @@ 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 PSMPreviewSwapExactOut_DaiAssetInTests is PSMTestBase { + + 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_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 + + mockRateProvider.__setConversionRate(conversionRate); + + uint256 expectedAmountIn = amountOut * conversionRate / 1e27; + + uint256 amountIn = psm.previewSwapExactOut(address(dai), address(sDai), amountOut); + + // Allow for rounding error of 1 unit upwards + assertLe(amountIn - expectedAmountIn, 1); } } -contract PSMPreviewSwapUSDCAssetInTests is PSMTestBase { +contract PSMPreviewSwapExactIn_USDCAssetInTests 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_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_previewSwap_usdcToDai(uint256 amountIn) public view { + 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 +163,60 @@ 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 PSMPreviewSwapExactOut_USDCAssetInTests is PSMTestBase { + + 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_previewSwapExactOut_usdcToDai(uint256 amountOut) public view { + amountOut = _bound(amountOut, 0, DAI_TOKEN_MAX); + + 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 { + 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 + + mockRateProvider.__setConversionRate(conversionRate); + + // Using raw calculation to demo rounding + uint256 expectedAmountIn = amountOut * conversionRate / 1e27 / 1e12; + + uint256 amountIn = psm.previewSwapExactOut(address(usdc), address(sDai), amountOut); + + // Allow for rounding error of 1 unit upwards + assertLe(amountIn - expectedAmountIn, 1); } } -contract PSMPreviewSwapSDaiAssetInTests is PSMTestBase { +contract PSMPreviewSwapExactIn_SDaiAssetInTests 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_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_previewSwap_sDaiToDai(uint256 amountIn, uint256 conversionRate) public { + 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 +224,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 +241,51 @@ 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 + + mockRateProvider.__setConversionRate(conversionRate); + + uint256 expectedAmountIn = amountOut * 1e27 / conversionRate; + + 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 { + 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 + + mockRateProvider.__setConversionRate(conversionRate); + + uint256 expectedAmountIn = amountOut * 1e27 / conversionRate * 1e12; + + uint256 amountIn = psm.previewSwapExactOut(address(sDai), address(usdc), amountOut); + + // 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 new file mode 100644 index 0000000..95f4bb0 --- /dev/null +++ b/test/unit/SwapExactIn.t.sol @@ -0,0 +1,470 @@ +// 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 PSMSwapExactInFailureTests 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_swapExactIn_amountZero() public { + vm.expectRevert("PSM3/invalid-amountIn"); + psm.swapExactIn(address(usdc), address(sDai), 0, 0, receiver, 0); + } + + function test_swapExactIn_receiverZero() public { + vm.expectRevert("PSM3/invalid-receiver"); + psm.swapExactIn(address(usdc), address(sDai), 100e6, 80e18, address(0), 0); + } + + function test_swapExactIn_invalid_assetIn() public { + vm.expectRevert("PSM3/invalid-asset"); + psm.swapExactIn(makeAddr("other-token"), address(sDai), 100e6, 80e18, receiver, 0); + } + + function test_swapExactIn_invalid_assetOut() public { + vm.expectRevert("PSM3/invalid-asset"); + psm.swapExactIn(address(usdc), makeAddr("other-token"), 100e6, 80e18, receiver, 0); + } + + function test_swapExactIn_bothAsset0() public { + vm.expectRevert("PSM3/invalid-asset"); + psm.swapExactIn(address(dai), address(dai), 100e6, 80e18, receiver, 0); + } + + function test_swapExactIn_bothAsset1() public { + vm.expectRevert("PSM3/invalid-asset"); + psm.swapExactIn(address(usdc), address(usdc), 100e6, 80e18, receiver, 0); + } + + function test_swapExactIn_bothAsset2() public { + vm.expectRevert("PSM3/invalid-asset"); + psm.swapExactIn(address(sDai), address(sDai), 100e6, 80e18, receiver, 0); + } + + function test_swapExactIn_minAmountOutBoundary() public { + usdc.mint(swapper, 100e6); + + vm.startPrank(swapper); + + usdc.approve(address(psm), 100e6); + + uint256 expectedAmountOut = psm.previewSwapExactIn(address(usdc), address(sDai), 100e6); + + assertEq(expectedAmountOut, 80e18); + + vm.expectRevert("PSM3/amountOut-too-low"); + psm.swapExactIn(address(usdc), address(sDai), 100e6, 80e18 + 1, receiver, 0); + + psm.swapExactIn(address(usdc), address(sDai), 100e6, 80e18, receiver, 0); + } + + function test_swapExactIn_insufficientApproveBoundary() public { + usdc.mint(swapper, 100e6); + + vm.startPrank(swapper); + + usdc.approve(address(psm), 100e6 - 1); + + vm.expectRevert("SafeERC20/transfer-from-failed"); + psm.swapExactIn(address(usdc), address(sDai), 100e6, 80e18, receiver, 0); + + usdc.approve(address(psm), 100e6); + + psm.swapExactIn(address(usdc), address(sDai), 100e6, 80e18, receiver, 0); + } + + function test_swapExactIn_insufficientUserBalanceBoundary() public { + usdc.mint(swapper, 100e6 - 1); + + vm.startPrank(swapper); + + usdc.approve(address(psm), 100e6); + + vm.expectRevert("SafeERC20/transfer-from-failed"); + psm.swapExactIn(address(usdc), address(sDai), 100e6, 80e18, receiver, 0); + + usdc.mint(swapper, 1); + + psm.swapExactIn(address(usdc), address(sDai), 100e6, 80e18, receiver, 0); + } + + 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. + usdc.mint(swapper, 125e6 + 2); + + vm.startPrank(swapper); + + usdc.approve(address(psm), 125e6 + 2); + + uint256 expectedAmountOut = psm.previewSwapExactIn(address(usdc), address(sDai), 125e6 + 2); + + assertEq(expectedAmountOut, 100.000001e18); // More than balance of sDAI + + vm.expectRevert("SafeERC20/transfer-failed"); + psm.swapExactIn(address(usdc), address(sDai), 125e6 + 2, 100e18, receiver, 0); + + psm.swapExactIn(address(usdc), address(sDai), 125e6, 100e18, receiver, 0); + } + +} + +contract PSMSwapExactInSuccessTestsBase 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 _swapExactInTest( + 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); + + uint256 returnedAmountOut = psm.swapExactIn( + address(assetIn), + address(assetOut), + amountIn, + amountOut, + receiver_, + 0 + ); + + assertEq(returnedAmountOut, amountOut); + + 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 PSMSwapExactInDaiAssetInTests is PSMSwapExactInSuccessTestsBase { + + function test_swapExactIn_daiToUsdc_sameReceiver() public assertAtomicPsmValueDoesNotChange { + _swapExactInTest(dai, usdc, 100e18, 100e6, swapper, swapper); + } + + function test_swapExactIn_daiToSDai_sameReceiver() public assertAtomicPsmValueDoesNotChange { + _swapExactInTest(dai, sDai, 100e18, 80e18, swapper, swapper); + } + + function test_swapExactIn_daiToUsdc_differentReceiver() public assertAtomicPsmValueDoesNotChange { + _swapExactInTest(dai, usdc, 100e18, 100e6, swapper, receiver); + } + + function test_swapExactIn_daiToSDai_differentReceiver() public assertAtomicPsmValueDoesNotChange { + _swapExactInTest(dai, sDai, 100e18, 80e18, swapper, receiver); + } + + function testFuzz_swapExactIn_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; + _swapExactInTest(dai, usdc, amountIn, amountOut, fuzzSwapper, fuzzReceiver); + } + + function testFuzz_swapExactIn_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 + mockRateProvider.__setConversionRate(conversionRate); + + uint256 amountOut = amountIn * 1e27 / conversionRate; + + _swapExactInTest(dai, sDai, amountIn, amountOut, fuzzSwapper, fuzzReceiver); + } + +} + +contract PSMSwapExactInUsdcAssetInTests is PSMSwapExactInSuccessTestsBase { + + function test_swapExactIn_usdcToDai_sameReceiver() public assertAtomicPsmValueDoesNotChange { + _swapExactInTest(usdc, dai, 100e6, 100e18, swapper, swapper); + } + + function test_swapExactIn_usdcToSDai_sameReceiver() public assertAtomicPsmValueDoesNotChange { + _swapExactInTest(usdc, sDai, 100e6, 80e18, swapper, swapper); + } + + function test_swapExactIn_usdcToDai_differentReceiver() public assertAtomicPsmValueDoesNotChange { + _swapExactInTest(usdc, dai, 100e6, 100e18, swapper, receiver); + } + + function test_swapExactIn_usdcToSDai_differentReceiver() public assertAtomicPsmValueDoesNotChange { + _swapExactInTest(usdc, sDai, 100e6, 80e18, swapper, receiver); + } + + function testFuzz_swapExactIn_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; + _swapExactInTest(usdc, dai, amountIn, amountOut, fuzzSwapper, fuzzReceiver); + } + + function testFuzz_swapExactIn_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 + + mockRateProvider.__setConversionRate(conversionRate); + + uint256 amountOut = amountIn * 1e27 / conversionRate * 1e12; + + _swapExactInTest(usdc, sDai, amountIn, amountOut, fuzzSwapper, fuzzReceiver); + } + +} + +contract PSMSwapExactInSDaiAssetInTests is PSMSwapExactInSuccessTestsBase { + + function test_swapExactIn_sDaiToDai_sameReceiver() public assertAtomicPsmValueDoesNotChange { + _swapExactInTest(sDai, dai, 100e18, 125e18, swapper, swapper); + } + + function test_swapExactIn_sDaiToUsdc_sameReceiver() public assertAtomicPsmValueDoesNotChange { + _swapExactInTest(sDai, usdc, 100e18, 125e6, swapper, swapper); + } + + function test_swapExactIn_sDaiToDai_differentReceiver() public assertAtomicPsmValueDoesNotChange { + _swapExactInTest(sDai, dai, 100e18, 125e18, swapper, receiver); + } + + function test_swapExactIn_sDaiToUsdc_differentReceiver() public assertAtomicPsmValueDoesNotChange { + _swapExactInTest(sDai, usdc, 100e18, 125e6, swapper, receiver); + } + + function testFuzz_swapExactIn_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 + + mockRateProvider.__setConversionRate(conversionRate); + + uint256 amountOut = amountIn * conversionRate / 1e27; + + _swapExactInTest(sDai, dai, amountIn, amountOut, fuzzSwapper, fuzzReceiver); + } + + function testFuzz_swapExactIn_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 + + mockRateProvider.__setConversionRate(conversionRate); + + uint256 amountOut = amountIn * conversionRate / 1e27 / 1e12; + + _swapExactInTest(sDai, usdc, amountIn, amountOut, fuzzSwapper, fuzzReceiver); + } + +} + +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.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(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.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); + } + + 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 new file mode 100644 index 0000000..8a622a1 --- /dev/null +++ b/test/unit/SwapExactOut.t.sol @@ -0,0 +1,516 @@ +// 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: Using higher amount so transfer fails + usdc.mint(swapper, 125e6 + 1); + + vm.startPrank(swapper); + + usdc.approve(address(psm), 125e6 + 1); + + vm.expectRevert("SafeERC20/transfer-failed"); + psm.swapExactOut(address(usdc), address(sDai), 100e18 + 1, 125e6 + 1, 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 { + + 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 amountOut, + uint256 amountIn, + 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); + + uint256 returnedAmountIn = psm.swapExactOut( + address(assetIn), + address(assetOut), + amountOut, + amountIn, + receiver_, + 0 + ); + + assertEq(returnedAmountIn, amountIn); + + 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, 100e6, 100e18, swapper, swapper); + } + + function test_swapExactOut_daiToSDai_sameReceiver() public assertAtomicPsmValueDoesNotChange { + _swapExactOutTest(dai, sDai, 80e18, 100e18, swapper, swapper); + } + + function test_swapExactOut_daiToUsdc_differentReceiver() public assertAtomicPsmValueDoesNotChange { + _swapExactOutTest(dai, usdc, 100e6, 100e18, swapper, receiver); + } + + function test_swapExactOut_daiToSDai_differentReceiver() public assertAtomicPsmValueDoesNotChange { + _swapExactOutTest(dai, sDai, 80e18, 100e18, swapper, receiver); + } + + function testFuzz_swapExactOut_daiToUsdc( + uint256 amountOut, + address fuzzSwapper, + address fuzzReceiver + ) public { + vm.assume(fuzzSwapper != address(psm)); + vm.assume(fuzzReceiver != address(psm)); + vm.assume(fuzzReceiver != address(0)); + + 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 amountOut, + uint256 conversionRate, + address fuzzSwapper, + address fuzzReceiver + ) public { + vm.assume(fuzzSwapper != address(psm)); + vm.assume(fuzzReceiver != address(psm)); + vm.assume(fuzzReceiver != address(0)); + + amountOut = _bound(amountOut, 1, DAI_TOKEN_MAX); + conversionRate = _bound(conversionRate, 0.01e27, 100e27); // 1% to 10,000% conversion rate + mockRateProvider.__setConversionRate(conversionRate); + + uint256 amountIn = amountOut * conversionRate / 1e27; + + 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); + } + +} + +contract PSMSwapExactOutUsdcAssetInTests is PSMSwapExactOutSuccessTestsBase { + + function test_swapExactOut_usdcToDai_sameReceiver() public assertAtomicPsmValueDoesNotChange { + _swapExactOutTest(usdc, dai, 100e18, 100e6, swapper, swapper); + } + + function test_swapExactOut_usdcToSDai_sameReceiver() public assertAtomicPsmValueDoesNotChange { + _swapExactOutTest(usdc, sDai, 80e18, 100e6, swapper, swapper); + } + + function test_swapExactOut_usdcToDai_differentReceiver() public assertAtomicPsmValueDoesNotChange { + _swapExactOutTest(usdc, dai, 100e18, 100e6, swapper, receiver); + } + + function test_swapExactOut_usdcToSDai_differentReceiver() public assertAtomicPsmValueDoesNotChange { + _swapExactOutTest(usdc, sDai, 80e18, 100e6, swapper, receiver); + } + + function testFuzz_swapExactOut_usdcToDai( + uint256 amountOut, + address fuzzSwapper, + address fuzzReceiver + ) public { + vm.assume(fuzzSwapper != address(psm)); + vm.assume(fuzzReceiver != address(psm)); + vm.assume(fuzzReceiver != address(0)); + + amountOut = _bound(amountOut, 1, DAI_TOKEN_MAX); // Zero amount reverts + uint256 amountIn = amountOut / 1e12; + + 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( + uint256 amountOut, + uint256 conversionRate, + address fuzzSwapper, + address fuzzReceiver + ) public { + vm.assume(fuzzSwapper != address(psm)); + vm.assume(fuzzReceiver != address(psm)); + vm.assume(fuzzReceiver != address(0)); + + amountOut = _bound(amountOut, 1, SDAI_TOKEN_MAX); + conversionRate = _bound(conversionRate, 0.01e27, 100e27); // 1% to 10,000% conversion rate + + mockRateProvider.__setConversionRate(conversionRate); + + uint256 amountIn = amountOut * conversionRate / 1e27 / 1e12; + + 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); + } + +} + +contract PSMSwapExactOutSDaiAssetInTests is PSMSwapExactOutSuccessTestsBase { + + function test_swapExactOut_sDaiToDai_sameReceiver() public assertAtomicPsmValueDoesNotChange { + _swapExactOutTest(sDai, dai, 125e18, 100e18, swapper, swapper); + } + + function test_swapExactOut_sDaiToUsdc_sameReceiver() public assertAtomicPsmValueDoesNotChange { + _swapExactOutTest(sDai, usdc, 125e6, 100e18, swapper, swapper); + } + + function test_swapExactOut_sDaiToDai_differentReceiver() public assertAtomicPsmValueDoesNotChange { + _swapExactOutTest(sDai, dai, 125e18, 100e18, swapper, receiver); + } + + function test_swapExactOut_sDaiToUsdc_differentReceiver() public assertAtomicPsmValueDoesNotChange { + _swapExactOutTest(sDai, usdc, 125e6, 100e18, swapper, receiver); + } + + function testFuzz_swapExactOut_sDaiToDai( + uint256 amountOut, + uint256 conversionRate, + address fuzzSwapper, + address fuzzReceiver + ) public { + vm.assume(fuzzSwapper != address(psm)); + vm.assume(fuzzReceiver != address(psm)); + vm.assume(fuzzReceiver != address(0)); + + amountOut = _bound(amountOut, 1, DAI_TOKEN_MAX); + conversionRate = _bound(conversionRate, 0.01e27, 100e27); // 1% to 10,000% conversion rate + + mockRateProvider.__setConversionRate(conversionRate); + + uint256 amountIn = amountOut * 1e27 / conversionRate; + + 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( + uint256 amountOut, + uint256 conversionRate, + address fuzzSwapper, + address fuzzReceiver + ) public { + vm.assume(fuzzSwapper != address(psm)); + vm.assume(fuzzReceiver != address(psm)); + vm.assume(fuzzReceiver != address(0)); + + amountOut = _bound(amountOut, 1, USDC_TOKEN_MAX); + conversionRate = _bound(conversionRate, 0.01e27, 100e27); // 1% to 10,000% conversion rate + + mockRateProvider.__setConversionRate(conversionRate); + + uint256 amountIn = amountOut * 1e27 / conversionRate * 1e12; + + 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.getPsmTotalValue(); + + 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.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"); + } + +} diff --git a/test/unit/Swaps.t.sol b/test/unit/Swaps.t.sol deleted file mode 100644 index 67739d8..0000000 --- a/test/unit/Swaps.t.sol +++ /dev/null @@ -1,346 +0,0 @@ -// 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, MockRateProvider, PSMTestBase } from "test/PSMTestBase.sol"; - -contract PSMSwapFailureTests 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_swap_amountZero() public { - vm.expectRevert("PSM3/invalid-amountIn"); - psm.swap(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); - } - - function test_swap_invalid_assetIn() public { - vm.expectRevert("PSM3/invalid-asset"); - psm.swap(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); - } - - function test_swap_bothAsset0() public { - vm.expectRevert("PSM3/invalid-asset"); - psm.swap(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); - } - - function test_swap_bothAsset2() public { - vm.expectRevert("PSM3/invalid-asset"); - psm.swap(address(sDai), address(sDai), 100e6, 80e18, receiver, 0); - } - - function test_swap_minAmountOutBoundary() public { - usdc.mint(swapper, 100e6); - - vm.startPrank(swapper); - - usdc.approve(address(psm), 100e6); - - uint256 expectedAmountOut = psm.previewSwap(address(usdc), address(sDai), 100e6); - - assertEq(expectedAmountOut, 80e18); - - vm.expectRevert("PSM3/amountOut-too-low"); - psm.swap(address(usdc), address(sDai), 100e6, 80e18 + 1, receiver, 0); - - psm.swap(address(usdc), address(sDai), 100e6, 80e18, receiver, 0); - } - - function test_swap_insufficientApproveBoundary() public { - usdc.mint(swapper, 100e6); - - vm.startPrank(swapper); - - usdc.approve(address(psm), 100e6 - 1); - - vm.expectRevert("SafeERC20/transfer-from-failed"); - psm.swap(address(usdc), address(sDai), 100e6, 80e18, receiver, 0); - - usdc.approve(address(psm), 100e6); - - psm.swap(address(usdc), address(sDai), 100e6, 80e18, receiver, 0); - } - - function test_swap_insufficientUserBalanceBoundary() public { - usdc.mint(swapper, 100e6 - 1); - - vm.startPrank(swapper); - - usdc.approve(address(psm), 100e6); - - vm.expectRevert("SafeERC20/transfer-from-failed"); - psm.swap(address(usdc), address(sDai), 100e6, 80e18, receiver, 0); - - usdc.mint(swapper, 1); - - psm.swap(address(usdc), address(sDai), 100e6, 80e18, receiver, 0); - } - - function test_swap_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. - usdc.mint(swapper, 125e6 + 2); - - vm.startPrank(swapper); - - usdc.approve(address(psm), 125e6 + 2); - - uint256 expectedAmountOut = psm.previewSwap(address(usdc), address(sDai), 125e6 + 2); - - 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.swap(address(usdc), address(sDai), 125e6, 100e18, receiver, 0); - } - -} - -contract PSMSwapSuccessTestsBase 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 _swapTest( - 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.swap(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 PSMSwapDaiAssetInTests is PSMSwapSuccessTestsBase { - - function test_swap_daiToUsdc_sameReceiver() public assertAtomicPsmValueDoesNotChange { - _swapTest(dai, usdc, 100e18, 100e6, swapper, swapper); - } - - function test_swap_daiToSDai_sameReceiver() public assertAtomicPsmValueDoesNotChange { - _swapTest(dai, sDai, 100e18, 80e18, swapper, swapper); - } - - function test_swap_daiToUsdc_differentReceiver() public assertAtomicPsmValueDoesNotChange { - _swapTest(dai, usdc, 100e18, 100e6, swapper, receiver); - } - - function test_swap_daiToSDai_differentReceiver() public assertAtomicPsmValueDoesNotChange { - _swapTest(dai, sDai, 100e18, 80e18, swapper, receiver); - } - - function testFuzz_swap_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; - _swapTest(dai, usdc, amountIn, amountOut, fuzzSwapper, fuzzReceiver); - } - - function testFuzz_swap_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 - mockRateProvider.__setConversionRate(conversionRate); - - uint256 amountOut = amountIn * 1e27 / conversionRate; - - _swapTest(dai, sDai, amountIn, amountOut, fuzzSwapper, fuzzReceiver); - } - -} - -contract PSMSwapUsdcAssetInTests is PSMSwapSuccessTestsBase { - - function test_swap_usdcToDai_sameReceiver() public assertAtomicPsmValueDoesNotChange { - _swapTest(usdc, dai, 100e6, 100e18, swapper, swapper); - } - - function test_swap_usdcToSDai_sameReceiver() public assertAtomicPsmValueDoesNotChange { - _swapTest(usdc, sDai, 100e6, 80e18, swapper, swapper); - } - - function test_swap_usdcToDai_differentReceiver() public assertAtomicPsmValueDoesNotChange { - _swapTest(usdc, dai, 100e6, 100e18, swapper, receiver); - } - - function test_swap_usdcToSDai_differentReceiver() public assertAtomicPsmValueDoesNotChange { - _swapTest(usdc, sDai, 100e6, 80e18, swapper, receiver); - } - - function testFuzz_swap_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; - _swapTest(usdc, dai, amountIn, amountOut, fuzzSwapper, fuzzReceiver); - } - - function testFuzz_swap_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 - - mockRateProvider.__setConversionRate(conversionRate); - - uint256 amountOut = amountIn * 1e27 / conversionRate * 1e12; - - _swapTest(usdc, sDai, amountIn, amountOut, fuzzSwapper, fuzzReceiver); - } - -} - -contract PSMSwapSDaiAssetInTests is PSMSwapSuccessTestsBase { - - function test_swap_sDaiToDai_sameReceiver() public assertAtomicPsmValueDoesNotChange { - _swapTest(sDai, dai, 100e18, 125e18, swapper, swapper); - } - - function test_swap_sDaiToUsdc_sameReceiver() public assertAtomicPsmValueDoesNotChange { - _swapTest(sDai, usdc, 100e18, 125e6, swapper, swapper); - } - - function test_swap_sDaiToDai_differentReceiver() public assertAtomicPsmValueDoesNotChange { - _swapTest(sDai, dai, 100e18, 125e18, swapper, receiver); - } - - function test_swap_sDaiToUsdc_differentReceiver() public assertAtomicPsmValueDoesNotChange { - _swapTest(sDai, usdc, 100e18, 125e6, swapper, receiver); - } - - function testFuzz_swap_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 - - mockRateProvider.__setConversionRate(conversionRate); - - uint256 amountOut = amountIn * conversionRate / 1e27; - - _swapTest(sDai, dai, amountIn, amountOut, fuzzSwapper, fuzzReceiver); - } - - function testFuzz_swap_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 - - mockRateProvider.__setConversionRate(conversionRate); - - uint256 amountOut = amountIn * conversionRate / 1e27 / 1e12; - - _swapTest(sDai, usdc, amountIn, amountOut, fuzzSwapper, fuzzReceiver); - } - -} diff --git a/test/unit/Withdraw.t.sol b/test/unit/Withdraw.t.sol index e2a65ed..4a4e72a 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);