From b02926de9c3c5dea61754756fcc69deb66047145 Mon Sep 17 00:00:00 2001 From: Lucas Manuel Date: Mon, 29 Jul 2024 09:17:14 -0400 Subject: [PATCH] feat: Add preview deposit withdraw tests (SC-499) (#26) * feat: add first tests * fix: rm duplicate file * feat: update preview deposit coverage * feat: start on withdraw tests * feat: unit tests complete * feat: add fuzz test * fix: update file names --- test/invariant/handlers/RateSetterHandler.sol | 1 + test/unit/PreviewDeposit.t.sol | 131 +++++++++++++ test/unit/PreviewWIthdraw.t.sol | 183 ++++++++++++++++++ .../{Previews.t.sol => SwapPreviews.t.sol} | 0 4 files changed, 315 insertions(+) create mode 100644 test/unit/PreviewDeposit.t.sol create mode 100644 test/unit/PreviewWIthdraw.t.sol rename test/unit/{Previews.t.sol => SwapPreviews.t.sol} (100%) diff --git a/test/invariant/handlers/RateSetterHandler.sol b/test/invariant/handlers/RateSetterHandler.sol index 5d58109..9eb0702 100644 --- a/test/invariant/handlers/RateSetterHandler.sol +++ b/test/invariant/handlers/RateSetterHandler.sol @@ -26,4 +26,5 @@ contract RateSetterHandler is StdUtils { setRateCount++; } + } diff --git a/test/unit/PreviewDeposit.t.sol b/test/unit/PreviewDeposit.t.sol new file mode 100644 index 0000000..17382b9 --- /dev/null +++ b/test/unit/PreviewDeposit.t.sol @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; + +import { MockRateProvider, PSMTestBase } from "test/PSMTestBase.sol"; + +contract PSMPreviewDeposit_FailureTests is PSMTestBase { + + function test_previewDeposit_invalidAsset() public { + vm.expectRevert("PSM3/invalid-asset"); + psm.previewDeposit(makeAddr("other-token"), 1); + } + +} + +contract PSMPreviewDeposit_SuccessTests is PSMTestBase { + + address depositor = makeAddr("depositor"); + + function test_previewDeposit_dai_firstDeposit() public view { + assertEq(psm.previewDeposit(address(dai), 1), 1); + assertEq(psm.previewDeposit(address(dai), 2), 2); + assertEq(psm.previewDeposit(address(dai), 3), 3); + + assertEq(psm.previewDeposit(address(dai), 1e18), 1e18); + assertEq(psm.previewDeposit(address(dai), 2e18), 2e18); + assertEq(psm.previewDeposit(address(dai), 3e18), 3e18); + } + + function testFuzz_previewDeposit_dai_firstDeposit(uint256 amount) public view { + amount = _bound(amount, 0, DAI_TOKEN_MAX); + assertEq(psm.previewDeposit(address(dai), amount), amount); + } + + function test_previewDeposit_usdc_firstDeposit() public view { + assertEq(psm.previewDeposit(address(usdc), 1), 1e12); + assertEq(psm.previewDeposit(address(usdc), 2), 2e12); + assertEq(psm.previewDeposit(address(usdc), 3), 3e12); + + assertEq(psm.previewDeposit(address(usdc), 1e6), 1e18); + assertEq(psm.previewDeposit(address(usdc), 2e6), 2e18); + assertEq(psm.previewDeposit(address(usdc), 3e6), 3e18); + } + + function testFuzz_previewDeposit_usdc_firstDeposit(uint256 amount) public view { + amount = _bound(amount, 0, USDC_TOKEN_MAX); + assertEq(psm.previewDeposit(address(usdc), amount), amount * 1e12); + } + + function test_previewDeposit_sDai_firstDeposit() public view { + assertEq(psm.previewDeposit(address(sDai), 1), 1); + assertEq(psm.previewDeposit(address(sDai), 2), 2); + assertEq(psm.previewDeposit(address(sDai), 3), 3); + assertEq(psm.previewDeposit(address(sDai), 4), 5); + + assertEq(psm.previewDeposit(address(sDai), 1e18), 1.25e18); + assertEq(psm.previewDeposit(address(sDai), 2e18), 2.50e18); + assertEq(psm.previewDeposit(address(sDai), 3e18), 3.75e18); + assertEq(psm.previewDeposit(address(sDai), 4e18), 5.00e18); + } + + function testFuzz_previewDeposit_sDai_firstDeposit(uint256 amount) public view { + amount = _bound(amount, 0, SDAI_TOKEN_MAX); + assertEq(psm.previewDeposit(address(sDai), amount), amount * 1.25e27 / 1e27); + } + + function test_previewDeposit_afterDepositsAndExchangeRateIncrease() public { + _assertOneToOne(); + + _deposit(address(dai), depositor, 1e18); + _assertOneToOne(); + + _deposit(address(usdc), depositor, 1e6); + _assertOneToOne(); + + _deposit(address(sDai), depositor, 0.8e18); + _assertOneToOne(); + + mockRateProvider.__setConversionRate(2e27); + + // $300 dollars of value deposited, 300 shares minted. + // sDAI portion becomes worth $160, full pool worth $360, each share worth $1.20 + // 1 USDC = 1/1.20 = 0.833... + assertEq(psm.previewDeposit(address(dai), 1e18), 0.833333333333333333e18); + assertEq(psm.previewDeposit(address(usdc), 1e6), 0.833333333333333333e18); + assertEq(psm.previewDeposit(address(sDai), 1e18), 1.666666666666666666e18); // 1 sDAI = $2 + } + + function testFuzz_previewDeposit_afterDepositsAndExchangeRateIncrease( + uint256 amount1, + uint256 amount2, + uint256 amount3, + uint256 conversionRate, + uint256 previewAmount + ) public { + amount1 = _bound(amount1, 1, DAI_TOKEN_MAX); + amount2 = _bound(amount2, 1, USDC_TOKEN_MAX); + amount3 = _bound(amount3, 1, SDAI_TOKEN_MAX); + conversionRate = _bound(conversionRate, 1.00e27, 1000e27); + previewAmount = _bound(previewAmount, 0, DAI_TOKEN_MAX); + + _assertOneToOne(); + + _deposit(address(dai), depositor, amount1); + _assertOneToOne(); + + _deposit(address(usdc), depositor, amount2); + _assertOneToOne(); + + _deposit(address(sDai), depositor, amount3); + _assertOneToOne(); + + mockRateProvider.__setConversionRate(conversionRate); + + uint256 totalSharesMinted = amount1 + amount2 * 1e12 + amount3 * 1.25e27 / 1e27; + uint256 totalValue = amount1 + amount2 * 1e12 + amount3 * conversionRate / 1e27; + uint256 usdcPreviewAmount = previewAmount / 1e12; + + assertEq(psm.previewDeposit(address(dai), previewAmount), previewAmount * totalSharesMinted / totalValue); + assertEq(psm.previewDeposit(address(usdc), usdcPreviewAmount), usdcPreviewAmount * 1e12 * totalSharesMinted / totalValue); // Divide then multiply to replicate rounding + assertEq(psm.previewDeposit(address(sDai), previewAmount), (previewAmount * conversionRate / 1e27) * totalSharesMinted / totalValue); + } + + function _assertOneToOne() internal view { + assertEq(psm.previewDeposit(address(dai), 1e18), 1e18); + assertEq(psm.previewDeposit(address(usdc), 1e6), 1e18); + assertEq(psm.previewDeposit(address(sDai), 1e18), 1.25e18); + } + +} diff --git a/test/unit/PreviewWIthdraw.t.sol b/test/unit/PreviewWIthdraw.t.sol new file mode 100644 index 0000000..64777da --- /dev/null +++ b/test/unit/PreviewWIthdraw.t.sol @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; + +import { MockRateProvider, PSMTestBase } from "test/PSMTestBase.sol"; + +contract PSMPreviewWithdraw_FailureTests is PSMTestBase { + + function test_previewWithdraw_invalidAsset() public { + vm.expectRevert("PSM3/invalid-asset"); + psm.previewWithdraw(makeAddr("other-token"), 1); + } + +} + +contract PSMPreviewWithdraw_ZeroAssetsTests is PSMTestBase { + + // Always returns zero because there is no balance of assets in the PSM in this case + function test_previewWithdraw_zeroTotalAssets() public { + ( uint256 shares1, uint256 assets1 ) = psm.previewWithdraw(address(dai), 1e18); + ( uint256 shares2, uint256 assets2 ) = psm.previewWithdraw(address(usdc), 1e6); + ( uint256 shares3, uint256 assets3 ) = psm.previewWithdraw(address(sDai), 1e18); + + assertEq(shares1, 0); + assertEq(assets1, 0); + assertEq(shares2, 0); + assertEq(assets2, 0); + assertEq(shares3, 0); + assertEq(assets3, 0); + + mockRateProvider.__setConversionRate(2e27); + + ( shares1, assets1 ) = psm.previewWithdraw(address(dai), 1e18); + ( shares2, assets2 ) = psm.previewWithdraw(address(usdc), 1e6); + ( shares3, assets3 ) = psm.previewWithdraw(address(sDai), 1e18); + + assertEq(shares1, 0); + assertEq(assets1, 0); + assertEq(shares2, 0); + assertEq(assets2, 0); + assertEq(shares3, 0); + assertEq(assets3, 0); + } + +} + +contract PSMPreviewWithdraw_SuccessTests is PSMTestBase { + + function setUp() public override { + super.setUp(); + // Setup so that address(this) has the most shares, higher underlying balance than PSM + // balance of sDAI and USDC + _deposit(address(dai), address(this), 100e18); + _deposit(address(usdc), makeAddr("usdc-user"), 10e6); + _deposit(address(sDai), makeAddr("sDai-user"), 1e18); + } + + function test_previewWithdraw_dai_amountLtUnderlyingBalance() public view { + ( uint256 shares, uint256 assets ) = psm.previewWithdraw(address(dai), 100e18 - 1); + assertEq(shares, 100e18 - 1); + assertEq(assets, 100e18 - 1); + } + + function test_previewWithdraw_dai_amountEqUnderlyingBalance() public view { + ( uint256 shares, uint256 assets ) = psm.previewWithdraw(address(dai), 100e18); + assertEq(shares, 100e18); + assertEq(assets, 100e18); + } + + function test_previewWithdraw_dai_amountGtUnderlyingBalance() public view { + ( uint256 shares, uint256 assets ) = psm.previewWithdraw(address(dai), 100e18 + 1); + assertEq(shares, 100e18); + assertEq(assets, 100e18); + } + + function test_previewWithdraw_usdc_amountLtUnderlyingBalanceAndLtPsmBalance() public view { + ( uint256 shares, uint256 assets ) = psm.previewWithdraw(address(usdc), 10e6 - 1); + assertEq(shares, 10e18 - 1e12); + assertEq(assets, 10e6 - 1); + } + + function test_previewWithdraw_usdc_amountLtUnderlyingBalanceAndEqPsmBalance() public view { + ( uint256 shares, uint256 assets ) = psm.previewWithdraw(address(usdc), 10e6); + assertEq(shares, 10e18); + assertEq(assets, 10e6); + } + + function test_previewWithdraw_usdc_amountLtUnderlyingBalanceAndGtPsmBalance() public view { + ( uint256 shares, uint256 assets ) = psm.previewWithdraw(address(usdc), 10e6 + 1); + assertEq(shares, 10e18); + assertEq(assets, 10e6); + } + + function test_previewWithdraw_sdai_amountLtUnderlyingBalanceAndLtPsmBalance() public view { + ( uint256 shares, uint256 assets ) = psm.previewWithdraw(address(sDai), 1e18 - 1); + assertEq(shares, 1.25e18 - 2); + assertEq(assets, 1e18 - 1); + } + + function test_previewWithdraw_sdai_amountLtUnderlyingBalanceAndEqPsmBalance() public view { + ( uint256 shares, uint256 assets ) = psm.previewWithdraw(address(sDai), 1e18); + assertEq(shares, 1.25e18); + assertEq(assets, 1e18); + } + + function test_previewWithdraw_sdai_amountLtUnderlyingBalanceAndGtPsmBalance() public view { + ( uint256 shares, uint256 assets ) = psm.previewWithdraw(address(sDai), 1e18 + 1); + assertEq(shares, 1.25e18); + assertEq(assets, 1e18); + } + +} + +contract PSMPreviewWithdraw_SuccessFuzzTests is PSMTestBase { + + struct TestParams { + uint256 amount1; + uint256 amount2; + uint256 amount3; + uint256 previewAmount1; + uint256 previewAmount2; + uint256 previewAmount3; + uint256 conversionRate; + } + + function testFuzz_previewWithdraw(TestParams memory params) public { + params.amount1 = _bound(params.amount1, 1, DAI_TOKEN_MAX); + params.amount2 = _bound(params.amount2, 1, USDC_TOKEN_MAX); + params.amount3 = _bound(params.amount3, 1, SDAI_TOKEN_MAX); + + // Only covering case of amount being below underlying to focus on value conversion + // and avoid reimplementation of contract logic for dealing with capping amounts + params.previewAmount1 = _bound(params.previewAmount1, 0, params.amount1); + params.previewAmount2 = _bound(params.previewAmount2, 0, params.amount2); + params.previewAmount3 = _bound(params.previewAmount3, 0, params.amount3); + + _deposit(address(dai), address(this), params.amount1); + _deposit(address(usdc), address(this), params.amount2); + _deposit(address(sDai), address(this), params.amount3); + + ( uint256 shares1, uint256 assets1 ) = psm.previewWithdraw(address(dai), params.previewAmount1); + ( uint256 shares2, uint256 assets2 ) = psm.previewWithdraw(address(usdc), params.previewAmount2); + ( uint256 shares3, uint256 assets3 ) = psm.previewWithdraw(address(sDai), params.previewAmount3); + + uint256 totalSharesMinted = params.amount1 + params.amount2 * 1e12 + params.amount3 * 1.25e27 / 1e27; + uint256 totalValue = totalSharesMinted; + + assertEq(shares1, params.previewAmount1 * totalSharesMinted / totalValue); + assertEq(shares2, params.previewAmount2 * 1e12 * totalSharesMinted / totalValue); + assertEq(shares3, params.previewAmount3 * 1.25e27 / 1e27 * totalSharesMinted / totalValue); + + assertEq(assets1, params.previewAmount1); + assertEq(assets2, params.previewAmount2); + assertEq(assets3, params.previewAmount3); + + params.conversionRate = _bound(params.conversionRate, 0.001e27, 1000e27); + mockRateProvider.__setConversionRate(params.conversionRate); + + // sDai value accrual changes the value of shares in the PSM + totalValue = params.amount1 + params.amount2 * 1e12 + params.amount3 * params.conversionRate / 1e27; + + ( shares1, assets1 ) = psm.previewWithdraw(address(dai), params.previewAmount1); + ( shares2, assets2 ) = psm.previewWithdraw(address(usdc), params.previewAmount2); + ( shares3, assets3 ) = psm.previewWithdraw(address(sDai), params.previewAmount3); + + uint256 sDaiConvertedAmount = params.previewAmount3 * params.conversionRate / 1e27; + + assertApproxEqAbs(shares1, params.previewAmount1 * totalSharesMinted / totalValue, 1); + assertApproxEqAbs(shares2, params.previewAmount2 * 1e12 * totalSharesMinted / totalValue, 1); + assertApproxEqAbs(shares3, sDaiConvertedAmount * totalSharesMinted / totalValue, 1); + + // Assert shares are always rounded up + assertGe(shares1, params.previewAmount1 * totalSharesMinted / totalValue); + assertGe(shares2, params.previewAmount2 * 1e12 * totalSharesMinted / totalValue); + assertGe(shares3, sDaiConvertedAmount * totalSharesMinted / totalValue); + + assertApproxEqAbs(assets1, params.previewAmount1, 1); + assertApproxEqAbs(assets2, params.previewAmount2, 1); + assertApproxEqAbs(assets3, params.previewAmount3, 1); + } + +} diff --git a/test/unit/Previews.t.sol b/test/unit/SwapPreviews.t.sol similarity index 100% rename from test/unit/Previews.t.sol rename to test/unit/SwapPreviews.t.sol