From 427a41b611fa615e89568423a935e3e1dbc26f7c Mon Sep 17 00:00:00 2001 From: Lucas Manuel Date: Wed, 26 Jun 2024 11:29:54 -0400 Subject: [PATCH 1/9] test: Add basic invariant testing (SC-459) (#7) * feat: first test working * feat: use larger numbers: * feat: test with initial burn amount passing * feat: update tests to work with updated burn logic, move conversion functions around and use previews * feat: remove todos * fix: update to remove console and update comment * feat: get swap tests working * feat: get all swap tests working * fix: update for three assets in logic * feat: all tests passing * fix: rm commented out test * feat: add preview swap tests * feat: move logic out of single use internal and use conversion rate everywhere * feat: move divRoundUp out of single use internal * feat: add full coverage for conversion tests * feat: add more preview cases * feat: refactor PSM to use three assets * fix: rm comment * feat: add interface, natspec, events, referral code, tests passing * fix: update to rm consolegp * feat: add events testing * feat: make precisions internal and add state var natspec * feat: finish natspec * feat: add readme * feat: add referral code note * fix: update constructor test * fix: update links * fix: reformatting * fix: update testing section * fix: improve overview * feat: add emojis * feat: remove all share burn logic, get all non inflation attack tests to pass * fix: cleanup diff * fix: update to use initial deposit instead of burn * feat: add readme section explaining attack * fix: minimize diff * fix: address bartek comments * feat: update all tests to work with new interfaces * feat: add deposit failure mode tests * feat: update to add assertions for return in deposit * feat: add withdraw failure tests * feat: update to address comments outside sharesToBurn * feat: update inflation attack test and readme * fix: update readme * feat: update test to constrain deposit/withdraw * feat: update to add both cases * feat: update per review * feat: update to use underscore bound, fix test * fix: typo * feat: add overrides, remove referrals, update referral type * fix: update expect emit * feat: update name and remove todos * feat: move files and set up structure * feat: update to rename files, contracts, and errors * fix: rm dup file, update toml * feat: get deposits working * chore: refactor into proper inheritance structure * feat: get all functions working with reverts * feat: update conversion * feat: get swaps working without reverts * feat: add fully working deposit/withdraw/swaps, invariant_B failing * ci: update for ci * fix: update name * chore: rm basly cased file * chore: re add * fix: re add invariant * ci: experiment with 2 million total calls * ci: add show progress flag * fix: move file back * ci: update verbosity * ci: add PR profile * fix: rm redundant files * feat: update from review changes * fix: update invariant * fix: add fuzz failure * chore: rm indexing comment * fix: rm redundant files from merge * feat: update to add seeding as part of invariants --- .github/workflows/{ci.yml => master.yml} | 7 +- .github/workflows/pr.yml | 87 ++++++++++++++++++++++ foundry.toml | 18 +++++ src/interfaces/IPSM3.sol | 2 - test/invariant/Invariants.t.sol | 80 ++++++++++++++++++++ test/invariant/handlers/HandlerBase.sol | 39 ++++++++++ test/invariant/handlers/LpHandler.sol | 63 ++++++++++++++++ test/invariant/handlers/SwapperHandler.sol | 86 +++++++++++++++++++++ test/{ => unit}/Constructor.t.sol | 2 +- test/{ => unit}/Conversions.t.sol | 2 +- test/{ => unit}/Deposit.t.sol | 2 +- test/{ => unit}/DoSAttack.t.sol | 0 test/{ => unit}/Events.t.sol | 0 test/{ => unit}/Getters.t.sol | 2 +- test/{ => unit}/InflationAttack.t.sol | 0 test/{ => unit}/Previews.t.sol | 0 test/{ => unit}/Rounding.t.sol | 0 test/{ => unit}/Swaps.t.sol | 2 +- test/{ => unit}/Withdraw.t.sol | 2 +- test/{ => unit}/harnesses/PSM3Harness.sol | 0 20 files changed, 381 insertions(+), 13 deletions(-) rename .github/workflows/{ci.yml => master.yml} (96%) create mode 100644 .github/workflows/pr.yml create mode 100644 test/invariant/Invariants.t.sol create mode 100644 test/invariant/handlers/HandlerBase.sol create mode 100644 test/invariant/handlers/LpHandler.sol create mode 100644 test/invariant/handlers/SwapperHandler.sol rename test/{ => unit}/Constructor.t.sol (98%) rename test/{ => unit}/Conversions.t.sol (99%) rename test/{ => unit}/Deposit.t.sol (99%) rename test/{ => unit}/DoSAttack.t.sol (100%) rename test/{ => unit}/Events.t.sol (100%) rename test/{ => unit}/Getters.t.sol (99%) rename test/{ => unit}/InflationAttack.t.sol (100%) rename test/{ => unit}/Previews.t.sol (100%) rename test/{ => unit}/Rounding.t.sol (100%) rename test/{ => unit}/Swaps.t.sol (99%) rename test/{ => unit}/Withdraw.t.sol (99%) rename test/{ => unit}/harnesses/PSM3Harness.sol (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/master.yml similarity index 96% rename from .github/workflows/ci.yml rename to .github/workflows/master.yml index 579beed..726353e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/master.yml @@ -1,11 +1,8 @@ name: CI on: - workflow_dispatch: - pull_request: push: - branches: - - master + branches: [master] env: FOUNDRY_PROFILE: ci @@ -40,7 +37,7 @@ jobs: 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: FOUNDRY_PROFILE=ci forge test + run: FOUNDRY_PROFILE=master forge test -vv --show-progress # coverage: # runs-on: ubuntu-latest diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml new file mode 100644 index 0000000..5614607 --- /dev/null +++ b/.github/workflows/pr.yml @@ -0,0 +1,87 @@ +name: CI + +on: [pull_request] + +env: + FOUNDRY_PROFILE: ci + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Build contracts + run: | + forge --version + forge build --sizes + + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Run tests + 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: FOUNDRY_PROFILE=pr forge test -vv --show-progress + + # coverage: + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v3 + + # - 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 + + # # 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. + + # - 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/foundry.toml b/foundry.toml index 4963041..d92768d 100644 --- a/foundry.toml +++ b/foundry.toml @@ -9,6 +9,24 @@ optimizer_runs = 200 [fuzz] runs = 1000 +[invariant] +runs = 20 +depth = 1000 + +[profile.pr.invariant] +runs = 200 +depth = 1000 + +[profile.pr.fuzz] +runs = 100_000 + +[profile.master.invariant] +runs = 200 +depth = 10_000 + +[profile.master.fuzz] +runs = 1_000_000 + # See more config options https://github.com/foundry-rs/foundry/tree/master/config remappings = [ diff --git a/src/interfaces/IPSM3.sol b/src/interfaces/IPSM3.sol index 6230014..06adef4 100644 --- a/src/interfaces/IPSM3.sol +++ b/src/interfaces/IPSM3.sol @@ -5,8 +5,6 @@ import { IERC20 } from "erc20-helpers/interfaces/IERC20.sol"; interface IPSM3 { - // TODO: Determine priority for indexing - /**********************************************************************************************/ /*** Events ***/ /**********************************************************************************************/ diff --git a/test/invariant/Invariants.t.sol b/test/invariant/Invariants.t.sol new file mode 100644 index 0000000..c2d7013 --- /dev/null +++ b/test/invariant/Invariants.t.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; + +import { PSMTestBase } from "test/PSMTestBase.sol"; + +import { LpHandler } from "test/invariant/handlers/LpHandler.sol"; +import { SwapperHandler } from "test/invariant/handlers/SwapperHandler.sol"; + +contract PSMInvariantTests is PSMTestBase { + + LpHandler public lpHandler; + SwapperHandler public swapperHandler; + + address BURN_ADDRESS = makeAddr("burn-address"); + + // NOTE [CRITICAL]: All invariant tests are operating under the assumption that the initial seed + // deposit of 1e18 shares has been made. This is a key requirement and + // assumption for all invariant tests. + function setUp() public override { + super.setUp(); + + // Seed the pool with 1e18 shares (1e18 of value) + _deposit(address(dai), BURN_ADDRESS, 1e18); + + lpHandler = new LpHandler(psm, dai, usdc, sDai, 3); + swapperHandler = new SwapperHandler(psm, dai, usdc, sDai, 3); + + // TODO: Add rate updates + rateProvider.__setConversionRate(1.25e27); + + targetContract(address(lpHandler)); + targetContract(address(swapperHandler)); + } + + function invariant_A() public view { + assertEq( + psm.shares(address(lpHandler.lps(0))) + + psm.shares(address(lpHandler.lps(1))) + + psm.shares(address(lpHandler.lps(2))) + + 1e18, // Seed amount + psm.totalShares() + ); + } + + function invariant_B() public view { + assertApproxEqAbs( + psm.getPsmTotalValue(), + psm.convertToAssetValue(psm.totalShares()), + 2 + ); + } + + function invariant_C() public view { + assertApproxEqAbs( + psm.convertToAssetValue(psm.shares(address(lpHandler.lps(0)))) + + psm.convertToAssetValue(psm.shares(address(lpHandler.lps(1)))) + + psm.convertToAssetValue(psm.shares(address(lpHandler.lps(2)))) + + psm.convertToAssetValue(1e18), // Seed amount + psm.getPsmTotalValue(), + 4 + ); + } + + function invariant_logs() public view { + console.log("depositCount ", lpHandler.depositCount()); + console.log("withdrawCount ", lpHandler.withdrawCount()); + console.log("swapCount ", swapperHandler.swapCount()); + console.log("zeroBalanceCount", swapperHandler.zeroBalanceCount()); + console.log( + "sum ", + lpHandler.depositCount() + + lpHandler.withdrawCount() + + swapperHandler.swapCount() + + swapperHandler.zeroBalanceCount() + ); + } + +} diff --git a/test/invariant/handlers/HandlerBase.sol b/test/invariant/handlers/HandlerBase.sol new file mode 100644 index 0000000..fbc8dff --- /dev/null +++ b/test/invariant/handlers/HandlerBase.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.13; + +import { MockERC20 } from "erc20-helpers/MockERC20.sol"; + +import { CommonBase } from "forge-std/Base.sol"; +import { StdCheatsSafe } from "forge-std/StdCheats.sol"; +import { StdUtils } from "forge-std/StdUtils.sol"; + +import { PSM3 } from "src/PSM3.sol"; + +contract HandlerBase is CommonBase, StdCheatsSafe, StdUtils { + + PSM3 public psm; + + MockERC20[3] public assets; + + constructor( + PSM3 psm_, + MockERC20 asset0, + MockERC20 asset1, + MockERC20 asset2 + ) { + psm = psm_; + + assets[0] = asset0; + assets[1] = asset1; + assets[2] = asset2; + } + + function _getAsset(uint256 indexSeed) internal view returns (MockERC20) { + return assets[indexSeed % assets.length]; + } + + function _hash(uint256 number_, string memory salt) internal pure returns (uint256 hash_) { + hash_ = uint256(keccak256(abi.encode(number_, salt))); + } + +} diff --git a/test/invariant/handlers/LpHandler.sol b/test/invariant/handlers/LpHandler.sol new file mode 100644 index 0000000..f321e2a --- /dev/null +++ b/test/invariant/handlers/LpHandler.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.13; + +import { MockERC20 } from "erc20-helpers/MockERC20.sol"; + +import { HandlerBase } from "test/invariant/handlers/HandlerBase.sol"; + +import { PSM3 } from "src/PSM3.sol"; + +contract LpHandler is HandlerBase { + + address[] public lps; + + uint256 public depositCount; + uint256 public withdrawCount; + + uint256 public constant TRILLION = 1e12; + + constructor( + PSM3 psm_, + MockERC20 asset0, + MockERC20 asset1, + MockERC20 asset2, + uint256 lpCount + ) HandlerBase(psm_, asset0, asset1, asset2) { + for (uint256 i = 0; i < lpCount; i++) { + lps.push(makeAddr(string(abi.encodePacked("lp-", i)))); + } + } + + function _getLP(uint256 indexSeed) internal view returns (address) { + return lps[indexSeed % lps.length]; + } + + function deposit(uint256 assetSeed, uint256 lpSeed, uint256 amount) public { + MockERC20 asset = _getAsset(assetSeed); + address lp = _getLP(lpSeed); + + amount = _bound(amount, 1, TRILLION * 10 ** asset.decimals()); + + vm.startPrank(lp); + asset.mint(lp, amount); + asset.approve(address(psm), amount); + psm.deposit(address(asset), lp, amount); + vm.stopPrank(); + + depositCount++; + } + + function withdraw(uint256 assetSeed, uint256 lpSeed, uint256 amount) public { + MockERC20 asset = _getAsset(assetSeed); + address lp = _getLP(lpSeed); + + amount = _bound(amount, 1, TRILLION * 10 ** asset.decimals()); + + vm.prank(lp); + psm.withdraw(address(asset), lp, amount); + vm.stopPrank(); + + withdrawCount++; + } + +} diff --git a/test/invariant/handlers/SwapperHandler.sol b/test/invariant/handlers/SwapperHandler.sol new file mode 100644 index 0000000..c0e2bb1 --- /dev/null +++ b/test/invariant/handlers/SwapperHandler.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.13; + +import { MockERC20 } from "erc20-helpers/MockERC20.sol"; + +import { HandlerBase } from "test/invariant/handlers/HandlerBase.sol"; + +import { PSM3 } from "src/PSM3.sol"; + +contract SwapperHandler is HandlerBase { + + address[] public swappers; + + uint256 public swapCount; + uint256 public zeroBalanceCount; + + constructor( + PSM3 psm_, + MockERC20 asset0, + MockERC20 asset1, + MockERC20 asset2, + uint256 lpCount + ) HandlerBase(psm_, asset0, asset1, asset2) { + for (uint256 i = 0; i < lpCount; i++) { + swappers.push(makeAddr(string(abi.encodePacked("swapper-", i)))); + } + } + + function _getSwapper(uint256 indexSeed) internal view returns (address) { + return swappers[indexSeed % swappers.length]; + } + + function swap( + uint256 assetInSeed, + uint256 assetOutSeed, + uint256 swapperSeed, + uint256 amountIn, + uint256 minAmountOut + ) + public + { + // Prevent overflow in if statement below + assetOutSeed = _bound(assetOutSeed, 0, type(uint256).max - 2); + + MockERC20 assetIn = _getAsset(assetInSeed); + MockERC20 assetOut = _getAsset(assetOutSeed); + address swapper = _getSwapper(swapperSeed); + + // Handle case where randomly selected assets match + if (assetIn == assetOut) { + assetOut = _getAsset(assetOutSeed + 2); + } + + // 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( + address(assetOut), + address(assetIn), + assetOut.balanceOf(address(psm)) + ); + + // If there's zero balance a swap can't be performed + if (maxAmountIn == 0) { + zeroBalanceCount++; + return; + } + + amountIn = _bound(amountIn, 1, maxAmountIn); + + // Fuzz between zero and the expected amount out from the swap + minAmountOut = _bound( + minAmountOut, + 0, + psm.previewSwap(address(assetIn), address(assetOut), amountIn) + ); + + vm.startPrank(swapper); + assetIn.mint(swapper, amountIn); + assetIn.approve(address(psm), amountIn); + psm.swap(address(assetIn), address(assetOut), amountIn, minAmountOut, swapper, 0); + vm.stopPrank(); + + swapCount++; + } + +} diff --git a/test/Constructor.t.sol b/test/unit/Constructor.t.sol similarity index 98% rename from test/Constructor.t.sol rename to test/unit/Constructor.t.sol index 1b3e22a..67ece94 100644 --- a/test/Constructor.t.sol +++ b/test/unit/Constructor.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.13; import "forge-std/Test.sol"; -import { PSM3 } from "../src/PSM3.sol"; +import { PSM3 } from "src/PSM3.sol"; import { PSMTestBase } from "test/PSMTestBase.sol"; diff --git a/test/Conversions.t.sol b/test/unit/Conversions.t.sol similarity index 99% rename from test/Conversions.t.sol rename to test/unit/Conversions.t.sol index 90f2e89..1b1c484 100644 --- a/test/Conversions.t.sol +++ b/test/unit/Conversions.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.13; import "forge-std/Test.sol"; -import { PSM3 } from "../src/PSM3.sol"; +import { PSM3 } from "src/PSM3.sol"; import { PSMTestBase } from "test/PSMTestBase.sol"; diff --git a/test/Deposit.t.sol b/test/unit/Deposit.t.sol similarity index 99% rename from test/Deposit.t.sol rename to test/unit/Deposit.t.sol index 0876ced..c85e54a 100644 --- a/test/Deposit.t.sol +++ b/test/unit/Deposit.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.13; import "forge-std/Test.sol"; -import { PSM3 } from "../src/PSM3.sol"; +import { PSM3 } from "src/PSM3.sol"; import { PSMTestBase } from "test/PSMTestBase.sol"; diff --git a/test/DoSAttack.t.sol b/test/unit/DoSAttack.t.sol similarity index 100% rename from test/DoSAttack.t.sol rename to test/unit/DoSAttack.t.sol diff --git a/test/Events.t.sol b/test/unit/Events.t.sol similarity index 100% rename from test/Events.t.sol rename to test/unit/Events.t.sol diff --git a/test/Getters.t.sol b/test/unit/Getters.t.sol similarity index 99% rename from test/Getters.t.sol rename to test/unit/Getters.t.sol index 2146bea..eb2c2e3 100644 --- a/test/Getters.t.sol +++ b/test/unit/Getters.t.sol @@ -5,7 +5,7 @@ import "forge-std/Test.sol"; import { PSMTestBase } from "test/PSMTestBase.sol"; -import { PSM3Harness } from "test/harnesses/PSM3Harness.sol"; +import { PSM3Harness } from "test/unit/harnesses/PSM3Harness.sol"; contract PSMHarnessTests is PSMTestBase { diff --git a/test/InflationAttack.t.sol b/test/unit/InflationAttack.t.sol similarity index 100% rename from test/InflationAttack.t.sol rename to test/unit/InflationAttack.t.sol diff --git a/test/Previews.t.sol b/test/unit/Previews.t.sol similarity index 100% rename from test/Previews.t.sol rename to test/unit/Previews.t.sol diff --git a/test/Rounding.t.sol b/test/unit/Rounding.t.sol similarity index 100% rename from test/Rounding.t.sol rename to test/unit/Rounding.t.sol diff --git a/test/Swaps.t.sol b/test/unit/Swaps.t.sol similarity index 99% rename from test/Swaps.t.sol rename to test/unit/Swaps.t.sol index 5db9680..43e4bbd 100644 --- a/test/Swaps.t.sol +++ b/test/unit/Swaps.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.13; import "forge-std/Test.sol"; -import { PSM3 } from "../src/PSM3.sol"; +import { PSM3 } from "src/PSM3.sol"; import { MockERC20, PSMTestBase } from "test/PSMTestBase.sol"; diff --git a/test/Withdraw.t.sol b/test/unit/Withdraw.t.sol similarity index 99% rename from test/Withdraw.t.sol rename to test/unit/Withdraw.t.sol index 041da77..e864ce6 100644 --- a/test/Withdraw.t.sol +++ b/test/unit/Withdraw.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.13; import "forge-std/Test.sol"; -import { PSM3 } from "../src/PSM3.sol"; +import { PSM3 } from "src/PSM3.sol"; import { MockERC20 } from "erc20-helpers/MockERC20.sol"; diff --git a/test/harnesses/PSM3Harness.sol b/test/unit/harnesses/PSM3Harness.sol similarity index 100% rename from test/harnesses/PSM3Harness.sol rename to test/unit/harnesses/PSM3Harness.sol From de1237d5897c0be8006b51a42de3d71b1c44ff4c Mon Sep 17 00:00:00 2001 From: Lucas Manuel Date: Wed, 26 Jun 2024 11:33:54 -0400 Subject: [PATCH 2/9] test: Add `convertTo` fuzz tests (SC-475) (#9) * feat: refactor to use helpers * feat: add for convert to shares * feat: add tests for dai * feat: add remaining tests * fix: update comments, add assertions --- test/unit/Conversions.t.sol | 533 +++++++++++++++++++++++++++++++++++- 1 file changed, 522 insertions(+), 11 deletions(-) diff --git a/test/unit/Conversions.t.sol b/test/unit/Conversions.t.sol index 1b1c484..114ffc6 100644 --- a/test/unit/Conversions.t.sol +++ b/test/unit/Conversions.t.sol @@ -7,7 +7,42 @@ import { PSM3 } from "src/PSM3.sol"; import { PSMTestBase } from "test/PSMTestBase.sol"; -// TODO: Add failure modes tests +contract PSMConversionTestBase is PSMTestBase { + + struct FuzzVars { + uint256 daiAmount; + uint256 usdcAmount; + uint256 sDaiAmount; + uint256 expectedShares; + } + + // Takes in fuzz inputs, bounds them, deposits assets, and returns + // initial shares from all deposits (always equal to total value at beginning). + function _setUpConversionFuzzTest( + uint256 initialConversionRate, + uint256 daiAmount, + uint256 usdcAmount, + uint256 sDaiAmount + ) + internal returns (FuzzVars memory vars) + { + vars.daiAmount = _bound(daiAmount, 1, DAI_TOKEN_MAX); + vars.usdcAmount = _bound(usdcAmount, 1, USDC_TOKEN_MAX); + vars.sDaiAmount = _bound(sDaiAmount, 1, SDAI_TOKEN_MAX); + + _deposit(address(dai), address(this), vars.daiAmount); + _deposit(address(usdc), address(this), vars.usdcAmount); + _deposit(address(sDai), address(this), vars.sDaiAmount); + + vars.expectedShares = + vars.daiAmount + + vars.usdcAmount * 1e12 + + vars.sDaiAmount * initialConversionRate / 1e27; + + // Assert that shares to be used for calcs are correct + assertEq(psm.totalShares(), vars.expectedShares); + } +} contract PSMConvertToAssetsTests is PSMTestBase { @@ -66,7 +101,7 @@ contract PSMConvertToAssetsTests is PSMTestBase { } -contract PSMConvertToAssetValueTests is PSMTestBase { +contract PSMConvertToAssetValueTests is PSMConversionTestBase { function testFuzz_convertToAssetValue_noValue(uint256 amount) public view { assertEq(psm.convertToAssetValue(amount), amount); @@ -86,11 +121,85 @@ contract PSMConvertToAssetValueTests is PSMTestBase { assertEq(psm.convertToAssetValue(1e18), 1.2e18); } - // TODO: Add fuzz test + function testFuzz_convertToAssetValue_conversionRateIncrease( + uint256 daiAmount, + uint256 usdcAmount, + uint256 sDaiAmount, + uint256 conversionRate + ) + public + { + rateProvider.__setConversionRate(1e27); // Start lower than 1.25 for this test + + FuzzVars memory vars = _setUpConversionFuzzTest( + 1e27, + daiAmount, + usdcAmount, + sDaiAmount + ); + + // These two values are always the same at the beginning + uint256 initialValue = vars.expectedShares; + + conversionRate = _bound(conversionRate, 1e27, 1000e27); + + // 1:1 between shares and dollar value + assertEq(psm.convertToAssetValue(vars.expectedShares), initialValue); + + rateProvider.__setConversionRate(conversionRate); + + uint256 newValue + = vars.daiAmount + vars.usdcAmount * 1e12 + vars.sDaiAmount * conversionRate / 1e27; + + assertEq(psm.convertToAssetValue(vars.expectedShares), newValue); + + // Value change is only from sDAI exchange rate increasing + assertEq(newValue - initialValue, vars.sDaiAmount * (conversionRate - 1e27) / 1e27); + } + + function testFuzz_convertToAssetValue_conversionRateDecrease( + uint256 daiAmount, + uint256 usdcAmount, + uint256 sDaiAmount, + uint256 conversionRate + ) + public + { + rateProvider.__setConversionRate(2e27); // Start higher than 1.25 for this test + + FuzzVars memory vars = _setUpConversionFuzzTest( + 2e27, + daiAmount, + usdcAmount, + sDaiAmount + ); + + // These two values are always the same at the beginning + uint256 initialValue = vars.expectedShares; + + conversionRate = _bound(conversionRate, 0.001e27, 2e27); + + // 1:1 between shares and dollar value + assertEq(psm.convertToAssetValue(vars.expectedShares), initialValue); + + rateProvider.__setConversionRate(conversionRate); + + uint256 newValue + = vars.daiAmount + vars.usdcAmount * 1e12 + vars.sDaiAmount * conversionRate / 1e27; + + assertEq(psm.convertToAssetValue(vars.expectedShares), newValue); + + // Value change is only from sDAI exchange rate decreasing + assertApproxEqAbs( + initialValue - newValue, + vars.sDaiAmount * (2e27 - conversionRate) / 1e27, + 1 + ); + } } -contract PSMConvertToSharesTests is PSMTestBase { +contract PSMConvertToSharesTests is PSMConversionTestBase { function test_convertToShares_noValue() public view { _assertOneToOneConversion(); @@ -116,7 +225,7 @@ contract PSMConvertToSharesTests is PSMTestBase { _assertOneToOneConversion(); } - function test_convertToShares_updateSDaiValue() public { + function test_convertToShares_conversionRateIncrease() public { // 200 shares minted at 1:1 ratio, $200 of value in pool _deposit(address(usdc), address(this), 100e6); _deposit(address(sDai), address(this), 80e18); @@ -136,6 +245,82 @@ contract PSMConvertToSharesTests is PSMTestBase { assertEq(psm.convertToShares(1.2e18), 1.090909090909090909e18); } + function testFuzz_convertToShares_conversionRateIncrease( + uint256 daiAmount, + uint256 usdcAmount, + uint256 sDaiAmount, + uint256 conversionRate + ) + public + { + rateProvider.__setConversionRate(1e27); // Start lower than 1.25 for this test + + FuzzVars memory vars = _setUpConversionFuzzTest( + 1e27, + daiAmount, + usdcAmount, + sDaiAmount + ); + + // These two values are always the same at the beginning + uint256 initialValue = vars.expectedShares; + + conversionRate = _bound(conversionRate, 1e27, 1000e27); + + // 1:1 between shares and dollar value + assertEq(psm.convertToShares(initialValue), vars.expectedShares); + + rateProvider.__setConversionRate(conversionRate); + + uint256 newValue + = vars.daiAmount + vars.usdcAmount * 1e12 + vars.sDaiAmount * conversionRate / 1e27; + + assertEq(psm.convertToShares(newValue), vars.expectedShares); + + // Value change is only from sDAI exchange rate increasing + assertEq(newValue - initialValue, vars.sDaiAmount * (conversionRate - 1e27) / 1e27); + } + + function testFuzz_convertToAssetValue_conversionRateDecrease( + uint256 daiAmount, + uint256 usdcAmount, + uint256 sDaiAmount, + uint256 conversionRate + ) + public + { + rateProvider.__setConversionRate(2e27); // Start higher than 1.25 for this test + + FuzzVars memory vars = _setUpConversionFuzzTest( + 2e27, + daiAmount, + usdcAmount, + sDaiAmount + ); + + // These two values are always the same at the beginning + uint256 initialValue = vars.expectedShares; + + conversionRate = _bound(conversionRate, 0.001e27, 2e27); + + // 1:1 between shares and dollar value + assertEq(psm.convertToShares(initialValue), vars.expectedShares); + + rateProvider.__setConversionRate(conversionRate); + + uint256 newValue + = vars.daiAmount + vars.usdcAmount * 1e12 + vars.sDaiAmount * conversionRate / 1e27; + + assertEq(psm.convertToShares(newValue), vars.expectedShares); + + // Value change is only from sDAI exchange rate decreasing + assertApproxEqAbs( + initialValue - newValue, + vars.sDaiAmount * (2e27 - conversionRate) / 1e27, + 1 + ); + } + function _assertOneToOneConversion() internal view { assertEq(psm.convertToShares(1), 1); assertEq(psm.convertToShares(2), 2); @@ -159,7 +344,7 @@ contract PSMConvertToSharesFailureTests is PSMTestBase { } -contract PSMConvertToSharesWithDaiTests is PSMTestBase { +contract PSMConvertToSharesWithDaiTests is PSMConversionTestBase { function test_convertToShares_noValue() public view { _assertOneToOneConversionDai(); @@ -186,7 +371,7 @@ contract PSMConvertToSharesWithDaiTests is PSMTestBase { _assertOneToOneConversionDai(); } - function test_convertToShares_updateSDaiValue() public { + function test_convertToShares_conversionRateIncrease() public { // 200 shares minted at 1:1 ratio, $200 of value in pool _deposit(address(dai), address(this), 100e18); _deposit(address(sDai), address(this), 80e18); @@ -206,6 +391,85 @@ contract PSMConvertToSharesWithDaiTests is PSMTestBase { assertEq(psm.convertToShares(address(dai), 12e18), 10.909090909090909090e18); } + // NOTE: These tests will be the exact same as convertToShares(amount) tests because DAI is an + // 18 decimal precision asset pegged to the dollar, which is whats used for "value". + + function testFuzz_convertToShares_conversionRateIncrease( + uint256 daiAmount, + uint256 usdcAmount, + uint256 sDaiAmount, + uint256 conversionRate + ) + public + { + rateProvider.__setConversionRate(1e27); // Start lower than 1.25 for this test + + FuzzVars memory vars = _setUpConversionFuzzTest( + 1e27, + daiAmount, + usdcAmount, + sDaiAmount + ); + + // These two values are always the same at the beginning + uint256 initialValue = vars.expectedShares; + + conversionRate = _bound(conversionRate, 1e27, 1000e27); + + // 1:1 between shares and dollar value + assertEq(psm.convertToShares(address(dai), initialValue), vars.expectedShares); + + rateProvider.__setConversionRate(conversionRate); + + uint256 newValue + = vars.daiAmount + vars.usdcAmount * 1e12 + vars.sDaiAmount * conversionRate / 1e27; + + assertEq(psm.convertToShares(address(dai), newValue), vars.expectedShares); + + // Value change is only from sDAI exchange rate increasing + assertEq(newValue - initialValue, vars.sDaiAmount * (conversionRate - 1e27) / 1e27); + } + + function testFuzz_convertToAssetValue_conversionRateDecrease( + uint256 daiAmount, + uint256 usdcAmount, + uint256 sDaiAmount, + uint256 conversionRate + ) + public + { + rateProvider.__setConversionRate(2e27); // Start higher than 1.25 for this test + + FuzzVars memory vars = _setUpConversionFuzzTest( + 2e27, + daiAmount, + usdcAmount, + sDaiAmount + ); + + // These two values are always the same at the beginning + uint256 initialValue = vars.expectedShares; + + conversionRate = _bound(conversionRate, 0.001e27, 2e27); + + // 1:1 between shares and dollar value + assertEq(psm.convertToShares(address(dai), initialValue), vars.expectedShares); + + rateProvider.__setConversionRate(conversionRate); + + uint256 newValue + = vars.daiAmount + vars.usdcAmount * 1e12 + vars.sDaiAmount * conversionRate / 1e27; + + assertEq(psm.convertToShares(address(dai), newValue), vars.expectedShares); + + // Value change is only from sDAI exchange rate decreasing + assertApproxEqAbs( + initialValue - newValue, + vars.sDaiAmount * (2e27 - conversionRate) / 1e27, + 1 + ); + } + function _assertOneToOneConversionDai() internal view { assertEq(psm.convertToShares(address(dai), 1), 1); assertEq(psm.convertToShares(address(dai), 2), 2); @@ -220,7 +484,7 @@ contract PSMConvertToSharesWithDaiTests is PSMTestBase { } -contract PSMConvertToSharesWithUsdcTests is PSMTestBase { +contract PSMConvertToSharesWithUsdcTests is PSMConversionTestBase { function test_convertToShares_noValue() public view { _assertOneToOneConversionUsdc(); @@ -247,7 +511,7 @@ contract PSMConvertToSharesWithUsdcTests is PSMTestBase { _assertOneToOneConversionUsdc(); } - function test_convertToShares_updateSDaiValue() public { + function test_convertToShares_conversionRateIncrease() public { // 200 shares minted at 1:1 ratio, $200 of value in pool _deposit(address(usdc), address(this), 100e6); _deposit(address(sDai), address(this), 80e18); @@ -267,6 +531,124 @@ contract PSMConvertToSharesWithUsdcTests is PSMTestBase { assertEq(psm.convertToShares(address(usdc), 12e6), 10.909090909090909090e18); } + function testFuzz_convertToShares_conversionRateIncrease( + uint256 daiAmount, + uint256 usdcAmount, + uint256 sDaiAmount, + uint256 conversionRate + ) + public + { + rateProvider.__setConversionRate(1e27); // Start lower than 1.25 for this test + + FuzzVars memory vars = _setUpConversionFuzzTest( + 1e27, + daiAmount, + usdcAmount, + sDaiAmount + ); + + // These two values are always the same at the beginning + uint256 initialValue = vars.expectedShares; + + conversionRate = _bound(conversionRate, 1e27, 1000e27); + + // Precision is lost when using 1e6 so expectedShares have to be adjusted accordingly + // but this represents a 1:1 exchange rate in 1e6 precision + assertEq( + psm.convertToShares(address(usdc), initialValue / 1e12), + vars.expectedShares / 1e12 * 1e12 + ); + + rateProvider.__setConversionRate(conversionRate); + + uint256 newValue + = vars.daiAmount + vars.usdcAmount * 1e12 + vars.sDaiAmount * conversionRate / 1e27; + + // Larger rounding error because of 1e6 precision + assertApproxEqAbs( + psm.convertToShares(address(usdc), newValue / 1e12), + vars.expectedShares, + 1e12 + ); + + // Make sure that rounding error here is always against the user + assertLe( + psm.convertToShares(address(usdc), newValue / 1e12), + vars.expectedShares + ); + + // This is the exact calculation of what is happening + assertEq( + psm.convertToShares(address(usdc), newValue / 1e12), + (newValue / 1e12 * 1e12) * vars.expectedShares / newValue + ); + + // Value change is only from sDAI exchange rate increasing + assertEq(newValue - initialValue, vars.sDaiAmount * (conversionRate - 1e27) / 1e27); + } + + function testFuzz_convertToAssetValue_conversionRateDecrease( + uint256 daiAmount, + uint256 usdcAmount, + uint256 sDaiAmount, + uint256 conversionRate + ) + public + { + rateProvider.__setConversionRate(2e27); // Start higher than 1.25 for this test + + FuzzVars memory vars = _setUpConversionFuzzTest( + 2e27, + daiAmount, + usdcAmount, + sDaiAmount + ); + + // These two values are always the same at the beginning + uint256 initialValue = vars.expectedShares; + + conversionRate = _bound(conversionRate, 0.001e27, 2e27); + + // Precision is lost when using 1e6 so expectedShares have to be adjusted accordingly + // but this represents a 1:1 exchange rate in 1e6 precision + assertEq( + psm.convertToShares(address(usdc), initialValue / 1e12), + vars.expectedShares / 1e12 * 1e12 + ); + + rateProvider.__setConversionRate(conversionRate); + + uint256 newValue + = vars.daiAmount + vars.usdcAmount * 1e12 + vars.sDaiAmount * conversionRate / 1e27; + + // Rounding scales with difference between expectedShares and newValue + assertApproxEqAbs( + psm.convertToShares(address(usdc), newValue / 1e12), + vars.expectedShares, + 1e12 + initialValue * 1e18 / newValue + ); + + // Make sure that rounding error here is always against the user + assertLe( + psm.convertToShares(address(usdc), newValue / 1e12), + vars.expectedShares + ); + + // This is the exact calculation of what is happening + assertEq( + psm.convertToShares(address(usdc), newValue / 1e12), + (newValue / 1e12 * 1e12) * vars.expectedShares / newValue + ); + + // Value change is only from sDAI exchange rate decreasing + assertApproxEqAbs( + initialValue - newValue, + vars.sDaiAmount * (2e27 - conversionRate) / 1e27, + 1 + ); + } + function _assertOneToOneConversionUsdc() internal view { assertEq(psm.convertToShares(address(usdc), 1), 1e12); assertEq(psm.convertToShares(address(usdc), 2), 2e12); @@ -281,7 +663,7 @@ contract PSMConvertToSharesWithUsdcTests is PSMTestBase { } -contract PSMConvertToSharesWithSDaiTests is PSMTestBase { +contract PSMConvertToSharesWithSDaiTests is PSMConversionTestBase { function test_convertToShares_noValue() public view { _assertOneToOneConversion(); @@ -312,7 +694,7 @@ contract PSMConvertToSharesWithSDaiTests is PSMTestBase { _assertStartingConversionSDai(); } - function test_convertToShares_updateSDaiValue() public { + function test_convertToShares_conversionRateIncrease() public { // 200 shares minted at 1:1 ratio, $200 of value in pool _deposit(address(usdc), address(this), 100e6); _deposit(address(sDai), address(this), 80e18); @@ -336,6 +718,135 @@ contract PSMConvertToSharesWithSDaiTests is PSMTestBase { assertEq(psm.convertToShares(address(sDai), 4e18), 5.454545454545454545e18); } + function testFuzz_convertToShares_conversionRateIncrease( + uint256 daiAmount, + uint256 usdcAmount, + uint256 sDaiAmount, + uint256 conversionRate + ) + public + { + // NOTE: Not using 1e27 for this test because initialSDaiValue needs to be different + rateProvider.__setConversionRate(1.1e27); // Start lower than 1.25 for this test + + FuzzVars memory vars = _setUpConversionFuzzTest( + 1.1e27, + daiAmount, + usdcAmount, + sDaiAmount + ); + + // These two values are always the same at the beginning + uint256 initialValue = vars.expectedShares; + uint256 initialSDaiValue = initialValue * 1e27 / 1.1e27; + + conversionRate = _bound(conversionRate, 1.1e27, 1000e27); + + // 1:1 between shares and dollar value + assertApproxEqAbs( + psm.convertToShares(address(sDai), initialSDaiValue), + vars.expectedShares, + 1 + ); + + rateProvider.__setConversionRate(conversionRate); + + uint256 newValue + = vars.daiAmount + vars.usdcAmount * 1e12 + vars.sDaiAmount * conversionRate / 1e27; + + uint256 newSDaiValue = newValue * 1e27 / conversionRate; + + // New conversion rate can be up to 1000x higher + assertApproxEqAbs( + psm.convertToShares(address(sDai), newSDaiValue), + vars.expectedShares, + 1000 + ); + + // Make sure that rounding error here is always against the user + assertLe( + psm.convertToShares(address(sDai), newSDaiValue), + vars.expectedShares + ); + + // This is the exact calculation of what is happening + assertEq( + psm.convertToShares(address(sDai), newSDaiValue), + (newSDaiValue * conversionRate / 1e27) * vars.expectedShares / newValue + ); + + // Value change is only from sDAI exchange rate increasing + assertApproxEqAbs( + newValue - initialValue, + vars.sDaiAmount * (conversionRate - 1.1e27) / 1e27, + 3 + ); + } + + function testFuzz_convertToAssetValue_conversionRateDecrease( + uint256 daiAmount, + uint256 usdcAmount, + uint256 sDaiAmount, + uint256 conversionRate + ) + public + { + rateProvider.__setConversionRate(2e27); // Start higher than 1.25 for this test + + FuzzVars memory vars = _setUpConversionFuzzTest( + 2e27, + daiAmount, + usdcAmount, + sDaiAmount + ); + + // These two values are always the same at the beginning + uint256 initialValue = vars.expectedShares; + uint256 initialSDaiValue = initialValue * 1e27 / 2e27; + + conversionRate = _bound(conversionRate, 0.001e27, 2e27); + + // 1:1 between shares and dollar value + assertApproxEqAbs( + psm.convertToShares(address(sDai), initialSDaiValue), + vars.expectedShares, + 1 + ); + + rateProvider.__setConversionRate(conversionRate); + + uint256 newValue + = vars.daiAmount + vars.usdcAmount * 1e12 + vars.sDaiAmount * conversionRate / 1e27; + + uint256 newSDaiValue = newValue * 1e27 / conversionRate; + + // New conversion rate can be up to 2000x lower (2e27 to 0.001e27) + assertApproxEqAbs( + psm.convertToShares(address(sDai), newSDaiValue), + vars.expectedShares, + 2000 + ); + + // Make sure that rounding error here is always against the user + assertLe( + psm.convertToShares(address(sDai), newSDaiValue), + vars.expectedShares + ); + + // This is the exact calculation of what is happening + assertEq( + psm.convertToShares(address(sDai), newSDaiValue), + (newSDaiValue * conversionRate / 1e27) * vars.expectedShares / newValue + ); + + // Value change is only from sDAI exchange rate increasing + assertApproxEqAbs( + initialValue - newValue, + vars.sDaiAmount * (2e27 - conversionRate) / 1e27, + 3 + ); + } + function _assertOneToOneConversion() internal view { assertEq(psm.convertToShares(1), 1); assertEq(psm.convertToShares(2), 2); From 481287f6dc4d5e70fb0c95e505e3a6679440a0b9 Mon Sep 17 00:00:00 2001 From: Lucas Manuel Date: Wed, 26 Jun 2024 11:37:21 -0400 Subject: [PATCH 3/9] test: Add deposit fuzz test (SC-477) (#13) * feat: add start of fuzz test * feat: working deposit test * fix: rm comment * test: decrease lower bounds --- test/unit/Deposit.t.sol | 101 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 100 insertions(+), 1 deletion(-) diff --git a/test/unit/Deposit.t.sol b/test/unit/Deposit.t.sol index c85e54a..bacb4b8 100644 --- a/test/unit/Deposit.t.sol +++ b/test/unit/Deposit.t.sol @@ -328,6 +328,105 @@ contract PSMDepositTests is PSMTestBase { assertEq(psm.getPsmTotalValue(), 400e18); } - // TODO: Add fuzz test + function testFuzz_deposit_multiUser_changeConversionRate( + uint256 usdcAmount, + uint256 sDaiAmount1, + uint256 sDaiAmount2, + uint256 newRate + ) + public + { + // Zero amounts revert + usdcAmount = _bound(usdcAmount, 1, USDC_TOKEN_MAX); + sDaiAmount1 = _bound(sDaiAmount1, 1, SDAI_TOKEN_MAX); + sDaiAmount2 = _bound(sDaiAmount2, 1, SDAI_TOKEN_MAX); + newRate = _bound(newRate, 1.25e27, 1000e27); + + uint256 user1DepositValue = usdcAmount * 1e12 + sDaiAmount1 * 125/100; + + usdc.mint(user1, usdcAmount); + + vm.startPrank(user1); + + usdc.approve(address(psm), usdcAmount); + + uint256 newShares = psm.deposit(address(usdc), receiver1, usdcAmount); + + assertEq(newShares, usdcAmount * 1e12); + + sDai.mint(user1, sDaiAmount1); + sDai.approve(address(psm), sDaiAmount1); + + newShares = psm.deposit(address(sDai), receiver1, sDaiAmount1); + + assertEq(newShares, sDaiAmount1 * 125/100); + + vm.stopPrank(); + + assertEq(usdc.balanceOf(address(psm)), usdcAmount); + + assertEq(sDai.balanceOf(user1), 0); + assertEq(sDai.balanceOf(address(psm)), sDaiAmount1); + + // Deposited at 1:1 conversion + uint256 receiver1Shares = user1DepositValue; + + assertEq(psm.totalShares(), receiver1Shares); + assertEq(psm.shares(user1), 0); + assertEq(psm.shares(receiver1), receiver1Shares); + + rateProvider.__setConversionRate(newRate); + + vm.startPrank(user2); + + sDai.mint(user2, sDaiAmount2); + sDai.approve(address(psm), sDaiAmount2); + + assertEq(sDai.allowance(user2, address(psm)), sDaiAmount2); + assertEq(sDai.balanceOf(user2), sDaiAmount2); + assertEq(sDai.balanceOf(address(psm)), sDaiAmount1); + + // Receiver1 has gained from conversion change + uint256 receiver1NewValue = user1DepositValue + sDaiAmount1 * (newRate - 1.25e27) / 1e27; + + // Receiver1 has gained from conversion change + assertApproxEqAbs( + psm.convertToAssetValue(psm.shares(receiver1)), + receiver1NewValue, + 1 + ); + + assertEq(psm.convertToAssetValue(psm.shares(receiver2)), 0); + + assertApproxEqAbs(psm.getPsmTotalValue(), receiver1NewValue, 1); + + newShares = psm.deposit(address(sDai), receiver2, sDaiAmount2); + + // Using queried values here instead of derived to avoid larger errors getting introduced + // Assertions above prove that these values are as expected. + uint256 receiver2Shares + = (sDaiAmount2 * newRate / 1e27) * psm.totalShares() / psm.getPsmTotalValue(); + + assertApproxEqAbs(newShares, receiver2Shares, 2); + + assertEq(sDai.allowance(user2, address(psm)), 0); + assertEq(sDai.balanceOf(user2), 0); + assertEq(sDai.balanceOf(address(psm)), sDaiAmount1 + sDaiAmount2); + + assertEq(psm.shares(user1), 0); + assertEq(psm.shares(user2), 0); + + assertApproxEqAbs(psm.totalShares(), receiver1Shares + receiver2Shares, 2); + assertApproxEqAbs(psm.shares(receiver1), receiver1Shares, 2); + assertApproxEqAbs(psm.shares(receiver2), receiver2Shares, 2); + + uint256 receiver2NewValue = sDaiAmount2 * newRate / 1e27; + + // Rate change of up to 1000x introduces errors + assertApproxEqAbs(psm.convertToAssetValue(psm.shares(receiver1)), receiver1NewValue, 1000); + assertApproxEqAbs(psm.convertToAssetValue(psm.shares(receiver2)), receiver2NewValue, 1000); + + assertApproxEqAbs(psm.getPsmTotalValue(), receiver1NewValue + receiver2NewValue, 1000); + } } From 1b6d44dc96c7240b0199f26ab30320f7fa0698d7 Mon Sep 17 00:00:00 2001 From: Lucas Manuel Date: Wed, 26 Jun 2024 11:56:31 -0400 Subject: [PATCH 4/9] test: Finalize withdraw tests (SC-478) (#12) * test: use convertToAssets * feat: add conversion rate to test * feat: finalize withdrawal testing * fix: relax tolerance --- test/unit/Withdraw.t.sol | 556 ++++++++++----------------------------- 1 file changed, 143 insertions(+), 413 deletions(-) diff --git a/test/unit/Withdraw.t.sol b/test/unit/Withdraw.t.sol index e864ce6..d5da864 100644 --- a/test/unit/Withdraw.t.sol +++ b/test/unit/Withdraw.t.sol @@ -427,474 +427,204 @@ contract PSMWithdrawTests is PSMTestBase { 0, usdcShareTolerance + 1 // 1 is added to the tolerance because of rounding error in sDAI calculations ); - - // -- TODO: Get these to work, rounding assertions proving always rounding down - - // assertLe(sDai.balanceOf(user2), expectedWithdrawnAmount3); - // assertGe(sDai.balanceOf(address(psm)), depositAmount3 - expectedWithdrawnAmount3); - - // assertLe( - // psm.shares(user2), - // (depositAmount2 * 1e12) + (depositAmount3 * 125/100) - (expectedWithdrawnAmount2 * 1e12) - (expectedWithdrawnAmount3 * 125/100) - // ); - - // assertLe( - // psm.totalShares(), - // totalValue - (expectedWithdrawnAmount1 + expectedWithdrawnAmount2) * 1e12 - (expectedWithdrawnAmount3 * 125/100) - // ); - } - - function _checkPsmInvariant() internal view { - uint256 totalSharesValue = psm.convertToAssetValue(psm.totalShares()); - uint256 totalAssetsValue = - sDai.balanceOf(address(psm)) * rateProvider.getConversionRate() / 1e27 - + usdc.balanceOf(address(psm)) * 1e12; - - assertApproxEqAbs(totalSharesValue, totalAssetsValue, 1); } - function _getExpectedWithdrawnAmount(MockERC20 asset, address user, uint256 amount) - internal view returns (uint256 withdrawAmount) - { - uint256 balance = asset.balanceOf(address(psm)); - uint256 userAssets = psm.convertToAssetValue(psm.shares(user)); - - if (address(asset) == address(usdc)) { - userAssets /= 1e12; - } - - if (address(asset) == address(sDai)) { - userAssets = userAssets * 1e27 / rateProvider.getConversionRate(); - } - - // Return the min of assets, balance, and amount - withdrawAmount = userAssets < balance ? userAssets : balance; - withdrawAmount = amount < withdrawAmount ? amount : withdrawAmount; - } - - // function test_withdraw_changeConversionRate_smallBalances_nonRoundingCode() public { - // _deposit(address(usdc), user1, 100e6); - // _deposit(address(sDai), user2, 100e18); - - // assertEq(psm.totalShares(), 225e18); - // assertEq(psm.shares(user1), 100e18); - // assertEq(psm.shares(user2), 125e18); - - // assertEq(psm.convertToShares(1e18), 1e18); - - // rateProvider.__setConversionRate(1.5e27); - - // // Total shares / (100 USDC + 150 sDAI value) - // uint256 expectedConversionRate = 225 * 1e18 / 250; - - // assertEq(expectedConversionRate, 0.9e18); - - // assertEq(psm.convertToShares(1e18), 0.9e18); - - // assertEq(usdc.balanceOf(user1), 0); - // assertEq(usdc.balanceOf(address(psm)), 100e6); - - // assertEq(psm.totalShares(), 225e18); - // assertEq(psm.shares(user1), 100e18); - // assertEq(psm.shares(user2), 125e18); - - // // Solving for `a` to get a result of 100.000001e6 USDC to transfer out - // // a * (250/225) / 1e12 = 100.000001e6 - // // a = 100.000001e6 * 1e12 / (250/225) - // // a = 100.000001e18 * (225/250) - // // Subtract 1 to get the amount that will succeed - // uint256 maxUsdcShares = 100.000001e18 * 0.9 - 1; - - // assertEq(maxUsdcShares, 90.0000009e18 - 1); - - // // NOTE: Users shares have more value than the balance of USDC now - // vm.startPrank(user1); - - // // Original full balance reverts - // vm.expectRevert("SafeERC20/transfer-failed"); - // psm.withdraw(address(usdc), 100e18); - - // // Boundary condition at 90.000001e18 shares - // vm.expectRevert("SafeERC20/transfer-failed"); - // psm.withdraw(address(usdc), maxUsdcShares + 1); - - // console2.log("First CTA", psm.convertToAssetValue(100e18)); - - // // Rounds down here and transfers 100e6 USDC - // psm.withdraw(address(usdc), maxUsdcShares); - - // console2.log("\n\n\n"); - - // assertEq(sDai.balanceOf(user1), 0); - // assertEq(sDai.balanceOf(address(psm)), 100e18); - - // assertEq(psm.totalShares(), 225e18 - maxUsdcShares); - // assertEq(psm.shares(user1), 100e18 - maxUsdcShares); - // assertEq(psm.shares(user2), 125e18); - - // console2.log("Second CTA", psm.convertToAssetValue(100e18)); - - // psm.withdraw(address(sDai), 100e18 - maxUsdcShares); - - // uint256 sDaiUser1Balance = 7.407406790123452675e18; - - // assertEq(sDai.balanceOf(user1), sDaiUser1Balance); - // assertEq(sDai.balanceOf(user2), 0); - // assertEq(sDai.balanceOf(address(psm)), 100e18 - sDaiUser1Balance); - - // assertEq(psm.totalShares(), 125e18); - // assertEq(psm.shares(user1), 0); - // assertEq(psm.shares(user2), 125e18); - - // vm.stopPrank(); - // vm.startPrank(user2); - - // console2.log("Third CTA", psm.convertToAssetValue(100e18)); - - // // Withdraw shares originally worth $100 to compare yield with user1 - // psm.withdraw(address(sDai), 125e18); - - // assertEq(sDai.balanceOf(user1), sDaiUser1Balance); - // assertEq(sDai.balanceOf(user2), 100e18 - sDaiUser1Balance - 1); - // assertEq(sDai.balanceOf(address(psm)), 1); - - // assertEq(psm.totalShares(), 0); - // assertEq(psm.shares(user1), 0); - // assertEq(psm.shares(user2), 0); - - // uint256 user1ResultingValue = usdc.balanceOf(user1) * 1e12 + sDai.balanceOf(user1) * 150/100; - // uint256 user2ResultingValue = sDai.balanceOf(user2) * 150/100; // Use 1.5 conversion rate - - // assertEq(user1ResultingValue, 111.111110185185179012e18); - // assertEq(user2ResultingValue, 138.888889814814820986e18); - - // assertEq(user1ResultingValue + user2ResultingValue, 249.999999999999999998e18); - - // // User1 gets a 0.000015% lower percentage yield than user2 - // assertEq((user1ResultingValue - 100e18) * 1e18 / 100e18, 0.111111101851851790e18); - // assertEq((user2ResultingValue - 125e18) * 1e18 / 125e18, 0.111111118518518567e18); - // } - - // function test_withdraw_changeConversionRate_bigBalances_roundingCode() public { - // _deposit(address(usdc), user1, 100_000_000e6); - // _deposit(address(sDai), user2, 100_000_000e18); - - // assertEq(psm.totalShares(), 225_000_000e18); - // assertEq(psm.shares(user1), 100_000_000e18); - // assertEq(psm.shares(user2), 125_000_000e18); - - // assertEq(psm.convertToShares(1e18), 1e18); - - // rateProvider.__setConversionRate(1.5e27); - - // // Total shares / (100 USDC + 150 sDAI value) - // uint256 expectedConversionRate = 225 * 1e18 / 250; - - // assertEq(expectedConversionRate, 0.9e18); - - // assertEq(psm.convertToShares(1e18), 0.9e18); - - // assertEq(usdc.balanceOf(user1), 0); - // assertEq(usdc.balanceOf(address(psm)), 100_000_000e6); - - // assertEq(psm.totalShares(), 225_000_000e18); - // assertEq(psm.shares(user1), 100_000_000e18); - // assertEq(psm.shares(user2), 125_000_000e18); - - // // Solving for `a` to get a result of 100.000001e6 USDC to transfer out - // // a * (250/225) / 1e12 = 100.000001e6 - // // a = 100.000001e6 * 1e12 / (250/225) - // // a = 100.000001e18 * (225/250) - // // Subtract 1 to get the amount that will succeed - // uint256 maxUsdcShares = 100_000_000.000001e18 * 0.9 - 1; - - // assertEq(maxUsdcShares, 90_000_000.0000009e18 - 1); - - // // NOTE: Users shares have more value than the balance of USDC now - // vm.startPrank(user1); - - // // Original full balance reverts - // vm.expectRevert("SafeERC20/transfer-failed"); - // psm.withdraw(address(usdc), 100_000_000e18); - - // // Boundary condition at 90.000001e18 shares - // vm.expectRevert("SafeERC20/transfer-failed"); - // psm.withdraw(address(usdc), maxUsdcShares + 1); - - // console2.log("First CTA", psm.convertToAssetValue(100_000_000e18)); - - // // Rounds down here and transfers 100_000_000e6 USDC - // psm.withdraw(address(usdc), maxUsdcShares); - - // console2.log("\n\n\n"); - - // console2.log("maxUsdcShares value", psm.convertToAssetValue(maxUsdcShares)); - - // assertEq(sDai.balanceOf(user1), 0); - // assertEq(sDai.balanceOf(address(psm)), 100_000_000e18); - - // maxUsdcShares = 90_000_000e18; - - // assertEq(psm.totalShares(), 225_000_000e18 - maxUsdcShares); - // assertEq(psm.shares(user1), 100_000_000e18 - maxUsdcShares); - // assertEq(psm.shares(user2), 125_000_000e18); - - // console2.log("Second CTA", psm.convertToAssetValue(100_000_000e18)); - - // psm.withdraw(address(sDai), 100_000_000e18 - maxUsdcShares); - - // uint256 sDaiUser1Balance = 7_407_407.407407407407407407e18; - - // assertEq(sDai.balanceOf(user1), sDaiUser1Balance); - // assertEq(sDai.balanceOf(user2), 0); - // assertEq(sDai.balanceOf(address(psm)), 100_000_000e18 - sDaiUser1Balance); - - // assertEq(psm.totalShares(), 125_000_000e18); - // assertEq(psm.shares(user1), 0); - // assertEq(psm.shares(user2), 125_000_000e18); - - // vm.stopPrank(); - // vm.startPrank(user2); - - // console2.log("Third CTA", psm.convertToAssetValue(100e18)); - - // // Withdraw shares originally worth $100 to compare yield with user1 - // psm.withdraw(address(sDai), 125_000_000e18); - - // assertEq(sDai.balanceOf(user1), sDaiUser1Balance); - // assertEq(sDai.balanceOf(user2), 100_000_000e18 - sDaiUser1Balance - 1); - // assertEq(sDai.balanceOf(address(psm)), 1); - - // assertEq(psm.totalShares(), 0); - // assertEq(psm.shares(user1), 0); - // assertEq(psm.shares(user2), 0); - - // uint256 user1ResultingValue = usdc.balanceOf(user1) * 1e12 + sDai.balanceOf(user1) * 150/100; - // uint256 user2ResultingValue = sDai.balanceOf(user2) * 150/100; // Use 1.5 conversion rate - - // console.log("\n\n FINAL RESULTS"); - // console.log("user1SDai", sDai.balanceOf(user1)); - - // assertEq(user1ResultingValue, 111_111_111.111111111111111110e18); - // assertEq(user2ResultingValue, 138_888_888.888888888888888888e18); - - // assertEq(user1ResultingValue + user2ResultingValue, 249_999_999.999999999999999998e18); - - // assertEq((user1ResultingValue - 100_000_000e18) * 1e18 / 100_000_000e18, 0.111111111111111111e18); - // assertEq((user2ResultingValue - 125_000_000e18) * 1e18 / 125_000_000e18, 0.111111111111111111e18); - // } - - // function test_withdraw_changeConversionRate_bigBalances_nonRoundingCode() public { - // _deposit(address(usdc), user1, 100_000_000e6); - // _deposit(address(sDai), user2, 100_000_000e18); - - // assertEq(psm.totalShares(), 225_000_000e18); - // assertEq(psm.shares(user1), 100_000_000e18); - // assertEq(psm.shares(user2), 125_000_000e18); - - // assertEq(psm.convertToShares(1e18), 1e18); - - // rateProvider.__setConversionRate(1.5e27); - - // // Total shares / (100 USDC + 150 sDAI value) - // uint256 expectedConversionRate = 225 * 1e18 / 250; - - // assertEq(expectedConversionRate, 0.9e18); - - // assertEq(psm.convertToShares(1e18), 0.9e18); - - // assertEq(usdc.balanceOf(user1), 0); - // assertEq(usdc.balanceOf(address(psm)), 100_000_000e6); - - // assertEq(psm.totalShares(), 225_000_000e18); - // assertEq(psm.shares(user1), 100_000_000e18); - // assertEq(psm.shares(user2), 125_000_000e18); - - // // Solving for `a` to get a result of 100.000001e6 USDC to transfer out - // // a * (250/225) / 1e12 = 100.000001e6 - // // a = 100.000001e6 * 1e12 / (250/225) - // // a = 100.000001e18 * (225/250) - // // Subtract 1 to get the amount that will succeed - // uint256 maxUsdcShares = 100_000_000.000001e18 * 0.9 - 1; - - // assertEq(maxUsdcShares, 90_000_000.0000009e18 - 1); - - // // NOTE: Users shares have more value than the balance of USDC now - // vm.startPrank(user1); - - // // Original full balance reverts - // vm.expectRevert("SafeERC20/transfer-failed"); - // psm.withdraw(address(usdc), 100_000_000e18); - - // // Boundary condition at 90.000001e18 shares - // vm.expectRevert("SafeERC20/transfer-failed"); - // psm.withdraw(address(usdc), maxUsdcShares + 1); - - // console2.log("First CTA", psm.convertToAssetValue(100_000_000e18)); + function test_withdraw_changeConversionRate() public { + _deposit(address(usdc), user1, 100e6); + _deposit(address(sDai), user2, 100e18); - // // Rounds down here and transfers 100_000_000e6 USDC - // psm.withdraw(address(usdc), maxUsdcShares); + assertEq(psm.convertToShares(1e18), 1e18); - // console2.log("\n\n\n"); + rateProvider.__setConversionRate(1.5e27); - // console2.log("maxUsdcShares value", psm.convertToAssetValue(maxUsdcShares)); + // Total shares / (100 USDC + 150 sDAI value) + uint256 expectedConversionRate = 225 * 1e18 / 250; - // assertEq(sDai.balanceOf(user1), 0); - // assertEq(sDai.balanceOf(address(psm)), 100_000_000e18); + assertEq(expectedConversionRate, 0.9e18); - // assertEq(psm.totalShares(), 225_000_000e18 - maxUsdcShares); - // assertEq(psm.shares(user1), 100_000_000e18 - maxUsdcShares); - // assertEq(psm.shares(user2), 125_000_000e18); + assertEq(psm.convertToShares(1e18), 0.9e18); - // console2.log("Second CTA", psm.convertToAssetValue(100_000_000e18)); + assertEq(usdc.balanceOf(user1), 0); + assertEq(usdc.balanceOf(address(psm)), 100e6); - // psm.withdraw(address(sDai), 100_000_000e18 - maxUsdcShares); + assertEq(psm.totalShares(), 225e18); + assertEq(psm.shares(user1), 100e18); + assertEq(psm.shares(user2), 125e18); - // uint256 sDaiUser1Balance = 7_407_407.407406790123456790e18; + // NOTE: Users shares have more value than the balance of USDC now + vm.prank(user1); + uint256 amount = psm.withdraw(address(usdc), user1, type(uint256).max); - // assertEq(sDai.balanceOf(user1), sDaiUser1Balance); - // assertEq(sDai.balanceOf(user2), 0); - // assertEq(sDai.balanceOf(address(psm)), 100_000_000e18 - sDaiUser1Balance); + assertEq(amount, 100e6); - // assertEq(psm.totalShares(), 125_000_000e18); - // assertEq(psm.shares(user1), 0); - // assertEq(psm.shares(user2), 125_000_000e18); + assertEq(usdc.balanceOf(user1), 100e6); + assertEq(usdc.balanceOf(address(psm)), 0); - // vm.stopPrank(); - // vm.startPrank(user2); + assertEq(sDai.balanceOf(user1), 0); + assertEq(sDai.balanceOf(user2), 0); + assertEq(sDai.balanceOf(address(psm)), 100e18); - // console2.log("Third CTA", psm.convertToAssetValue(100e18)); + assertEq(psm.totalShares(), 135e18); + assertEq(psm.shares(user1), 10e18); // Burn 90 shares to get 100 USDC + assertEq(psm.shares(user2), 125e18); - // // Withdraw shares originally worth $100 to compare yield with user1 - // psm.withdraw(address(sDai), 125_000_000e18); + vm.prank(user1); + amount = psm.withdraw(address(sDai), user1, type(uint256).max); - // assertEq(sDai.balanceOf(user1), sDaiUser1Balance); - // assertEq(sDai.balanceOf(user2), 100_000_000e18 - sDaiUser1Balance); - // assertEq(sDai.balanceOf(address(psm)), 0); + uint256 user1SDai = uint256(10e18) * 1e18 / 0.9e18 * 1e27 / 1.5e27; - // assertEq(psm.totalShares(), 0); - // assertEq(psm.shares(user1), 0); - // assertEq(psm.shares(user2), 0); + assertEq(amount, user1SDai); + assertEq(user1SDai, 7.407407407407407407e18); - // uint256 user1ResultingValue = usdc.balanceOf(user1) * 1e12 + sDai.balanceOf(user1) * 150/100; - // uint256 user2ResultingValue = sDai.balanceOf(user2) * 150/100; // Use 1.5 conversion rate + assertEq(sDai.balanceOf(user1), user1SDai); + assertEq(sDai.balanceOf(user2), 0); + assertEq(sDai.balanceOf(address(psm)), 100e18 - user1SDai); - // console.log("\n\n FINAL RESULTS"); - // console.log("user1SDai", sDai.balanceOf(user1)); + assertEq(psm.totalShares(), 125e18); + assertEq(psm.shares(user1), 0); + assertEq(psm.shares(user2), 125e18); - // assertEq(user1ResultingValue, 111_111_111.111110185185185185e18); - // assertEq(user2ResultingValue, 138_888_888.888889814814814815e18); + vm.prank(user2); + amount = psm.withdraw(address(sDai), user2, type(uint256).max); - // assertEq(user1ResultingValue + user2ResultingValue, 250_000_000e18); + assertEq(amount, 100e18 - user1SDai); // Remaining funds in PSM - // // User1 gets a 0.000000000015% lower percentage yield than user2 because the shares - // // that could've been used to withdraw sDAI with burned on the withdrawal. - // assertEq((user1ResultingValue - 100_000_000e18) * 1e18 / 100_000_000e18, 0.111111111111101851e18); - // assertEq((user2ResultingValue - 125_000_000e18) * 1e18 / 125_000_000e18, 0.111111111111118518e18); - // } + assertEq(sDai.balanceOf(user1), user1SDai); + assertEq(sDai.balanceOf(user2), 100e18 - user1SDai); + assertEq(sDai.balanceOf(address(psm)), 0); - // function test_withdraw_2() public { - // _deposit(address(usdc), user1, 100e6); - // _deposit(address(sDai), user2, 100e18); + assertEq(psm.totalShares(), 0); + assertEq(psm.shares(user1), 0); + assertEq(psm.shares(user2), 0); - // assertEq(psm.totalShares(), 225e18); - // assertEq(psm.shares(user1), 100e18); - // assertEq(psm.shares(user2), 125e18); + uint256 user1ResultingValue = usdc.balanceOf(user1) * 1e12 + sDai.balanceOf(user1) * 150/100; + uint256 user2ResultingValue = sDai.balanceOf(user2) * 150/100; // Use 1.5 conversion rate - // assertEq(psm.convertToShares(1e18), 1e18); + assertEq(user1ResultingValue, 111.111111111111111110e18); + assertEq(user2ResultingValue, 138.888888888888888889e18); - // rateProvider.__setConversionRate(1.5e27); + assertEq(user1ResultingValue + user2ResultingValue, 249.999999999999999999e18); - // // Total shares / (100 USDC + 150 sDAI value) - // uint256 expectedConversionRate = 225 * 1e18 / 250; + // Value gains are the same for both users + assertEq((user1ResultingValue - 100e18) * 1e18 / 100e18, 0.111111111111111111e18); + assertEq((user2ResultingValue - 125e18) * 1e18 / 125e18, 0.111111111111111111e18); + } - // assertEq(expectedConversionRate, 0.9e18); + function testFuzz_withdraw_changeConversionRate( + uint256 usdcAmount, + uint256 sDaiAmount, + uint256 conversionRate + ) + public + { + // Use higher lower bounds to get returns at the end to be more accurate + usdcAmount = _bound(usdcAmount, 1e6, USDC_TOKEN_MAX); + sDaiAmount = _bound(sDaiAmount, 1e18, SDAI_TOKEN_MAX); + conversionRate = _bound(conversionRate, 1.25e27, 1000e27); - // assertEq(psm.convertToShares(1e18), 0.9e18); + _deposit(address(usdc), user1, usdcAmount); + _deposit(address(sDai), user2, sDaiAmount); - // assertEq(usdc.balanceOf(user1), 0); - // assertEq(usdc.balanceOf(address(psm)), 100e6); + rateProvider.__setConversionRate(conversionRate); - // assertEq(psm.totalShares(), 225e18); - // assertEq(psm.shares(user1), 100e18); - // assertEq(psm.shares(user2), 125e18); + uint256 user1Shares = usdcAmount * 1e12; + uint256 user2Shares = sDaiAmount * 125/100; + uint256 totalShares = user1Shares + user2Shares; + uint256 totalValue = usdcAmount * 1e12 + sDaiAmount * conversionRate / 1e27; - // // Solving for `a` to get a result of 100.000001e6 USDC to transfer out - // // a * (250/225) / 1e12 = 100.000001e6 - // // a = 100.000001e6 * 1e12 / (250/225) - // // a = 100.000001e18 * (225/250) - // // Subtract 1 to get the amount that will succeed - // uint256 maxUsdcShares = 100.000001e18 * 0.9 - 1; + assertEq(psm.getPsmTotalValue(), totalValue); - // assertEq(maxUsdcShares, 90.0000009e18 - 1); + assertEq(psm.totalShares(), totalShares); + assertEq(psm.shares(user1), user1Shares); + assertEq(psm.shares(user2), user2Shares); - // // // NOTE: Users shares have more value than the balance of USDC now - // vm.startPrank(user1); + assertEq(usdc.balanceOf(user1), 0); + assertEq(usdc.balanceOf(address(psm)), usdcAmount); - // // // Original full balance reverts - // // vm.expectRevert("SafeERC20/transfer-failed"); - // // psm.withdraw(address(usdc), 100e18); + // NOTE: Users shares have more value than the balance of USDC now + vm.prank(user1); + uint256 amount = psm.withdraw(address(usdc), user1, type(uint256).max); - // // // Boundary condition at 90.000001e18 shares - // // vm.expectRevert("SafeERC20/transfer-failed"); - // // psm.withdraw(address(usdc), maxUsdcShares + 1); + assertEq(amount, usdcAmount); // Withdraws all USDC since shares are worth more - // console2.log("First CTA", psm.convertToAssetValue(100e18)); + assertEq(usdc.balanceOf(user1), usdcAmount); + assertEq(usdc.balanceOf(address(psm)), 0); - // // maxUsdcShares = 89.99999e18; + assertEq(sDai.balanceOf(user1), 0); + assertEq(sDai.balanceOf(user2), 0); + assertEq(sDai.balanceOf(address(psm)), sDaiAmount); - // // Rounds down here and transfers 100e6 USDC - // psm.withdraw(address(usdc), maxUsdcShares); + uint256 expectedUser1SharesBurned = usdcAmount * 1e12 * totalShares / totalValue; - // console2.log("\n\n\n"); + assertApproxEqAbs(psm.totalShares(), totalShares - expectedUser1SharesBurned, 2); + assertApproxEqAbs(psm.shares(user1), user1Shares - expectedUser1SharesBurned, 2); + assertApproxEqAbs(psm.shares(user2), user2Shares, 0); - // // assertEq(sDai.balanceOf(user1), 0); - // // assertEq(sDai.balanceOf(address(psm)), 100e18); + vm.prank(user1); + amount = psm.withdraw(address(sDai), user1, type(uint256).max); - // // assertEq(psm.totalShares(), 225e18 - maxUsdcShares); - // // assertEq(psm.shares(user1), 100e18 - maxUsdcShares); - // // assertEq(psm.shares(user2), 125e18); + // User1s remaining shares are used + uint256 user1SDai = (user1Shares - expectedUser1SharesBurned) + * totalValue + / totalShares + * 1e27 + / conversionRate; - // console2.log("Second CTA", psm.convertToAssetValue(100e18)); + assertApproxEqAbs(sDai.balanceOf(user1), user1SDai, 2); + assertApproxEqAbs(sDai.balanceOf(user2), 0, 0); + assertApproxEqAbs(sDai.balanceOf(address(psm)), sDaiAmount - user1SDai, 2); - // // psm.withdraw(address(sDai), 100e18 - maxUsdcShares); + vm.prank(user2); + amount = psm.withdraw(address(sDai), user2, type(uint256).max); - // // uint256 sDaiUser1Balance = 7.407406790123452675e18; + assertApproxEqAbs(amount, sDaiAmount - user1SDai, 2); - // // assertEq(sDai.balanceOf(user1), sDaiUser1Balance); - // // assertEq(sDai.balanceOf(user2), 0); - // // assertEq(sDai.balanceOf(address(psm)), 100e18 - sDaiUser1Balance); + assertApproxEqAbs(sDai.balanceOf(user1), user1SDai, 2); + assertApproxEqAbs(sDai.balanceOf(user2), sDaiAmount - user1SDai, 2); + assertApproxEqAbs(sDai.balanceOf(address(psm)), 0, 2); - // // assertEq(psm.totalShares(), 125e18); - // // assertEq(psm.shares(user1), 0); - // // assertEq(psm.shares(user2), 125e18); + assertLe(psm.totalShares(), 1); + assertLe(psm.shares(user1), 1); + assertLe(psm.shares(user2), 1); - // // vm.stopPrank(); - // // vm.startPrank(user2); + uint256 user1ResultingValue + = usdc.balanceOf(user1) * 1e12 + sDai.balanceOf(user1) * conversionRate / 1e27; - // // console2.log("Third CTA", psm.convertToAssetValue(100e18)); + uint256 user2ResultingValue = sDai.balanceOf(user2) * conversionRate / 1e27; // Use 1.5 conversion rate - // // // Withdraw shares originally worth $100 to compare yield with user1 - // // psm.withdraw(address(sDai), 100e18); + // Equal to starting value + assertApproxEqAbs(user1ResultingValue + user2ResultingValue, totalValue, 2); - // // // assertEq(sDai.balanceOf(user1), sDaiUser1Balance); - // // // assertEq(sDai.balanceOf(user2), 100e18 - sDaiUser1Balance - 1); - // // // assertEq(sDai.balanceOf(address(psm)), 1); + // Value gains are the same for both users, accurate to 0.01% + assertApproxEqRel( + (user1ResultingValue - (usdcAmount * 1e12)) * 1e18 / (usdcAmount * 1e12), + (user2ResultingValue - (sDaiAmount * 125/100)) * 1e18 / (sDaiAmount * 125/100), + 0.0001e18 + ); + } - // // // assertEq(psm.totalShares(), 0); - // // // assertEq(psm.shares(user1), 0); - // // // assertEq(psm.shares(user2), 0); + /**********************************************************************************************/ + /*** Helper functions ***/ + /**********************************************************************************************/ - // // uint256 user1ResultingValue = usdc.balanceOf(user1) * 1e12 + sDai.balanceOf(user1); - // // uint256 user2ResultingValue = sDai.balanceOf(user2) * 150/100; // Use 1.5 conversion rate + function _checkPsmInvariant() internal view { + uint256 totalSharesValue = psm.convertToAssetValue(psm.totalShares()); + uint256 totalAssetsValue = + sDai.balanceOf(address(psm)) * rateProvider.getConversionRate() / 1e27 + + usdc.balanceOf(address(psm)) * 1e12; - // // assertEq(user1ResultingValue, 107.407406790123452675e18); - // // assertEq(user2ResultingValue, 111.111111851851856788e18); + assertApproxEqAbs(totalSharesValue, totalAssetsValue, 1); + } - // // assertEq(user1ResultingValue + user2ResultingValue, 250e18); + function _getExpectedWithdrawnAmount(MockERC20 asset, address user, uint256 amount) + internal view returns (uint256 withdrawAmount) + { + uint256 balance = asset.balanceOf(address(psm)); + uint256 userAssets = psm.convertToAssets(address(asset), psm.shares(user)); - // // assertEq((user1ResultingValue - 100e18) * 1e18 / 100e18, 0.074074067901234526e18); - // // assertEq((user2ResultingValue - 125e18) * 1e18 / 125e18, 0.111111118518518567e18); - // } + // Return the min of assets, balance, and amount + withdrawAmount = userAssets < balance ? userAssets : balance; + withdrawAmount = amount < withdrawAmount ? amount : withdrawAmount; + } } From 3a8ede1ab472da2b878511bc9c6b5ce8a85c9d64 Mon Sep 17 00:00:00 2001 From: Lucas Manuel Date: Wed, 26 Jun 2024 12:31:56 -0400 Subject: [PATCH 5/9] fix: Update badge (#18) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dd593b2..61d1a87 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ⚡ Spark PSM ⚡ -![Foundry CI](https://github.com/marsfoundation/spark-psm/actions/workflows/ci.yml/badge.svg) +![Foundry CI](https://github.com/marsfoundation/spark-psm/actions/workflows/master.yml/badge.svg) [![Foundry][foundry-badge]][foundry] [![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](https://github.com/marsfoundation/spark-psm/blob/master/LICENSE) From dd5650f83cc66deb2da363d8ac1cfc88599b4aec Mon Sep 17 00:00:00 2001 From: Lucas Manuel Date: Tue, 2 Jul 2024 09:58:39 -0400 Subject: [PATCH 6/9] feat: Add invariant hooks, refactor to handle multiple suites (SC-481) (#14) * feat: first test working * feat: use larger numbers: * feat: test with initial burn amount passing * feat: update tests to work with updated burn logic, move conversion functions around and use previews * feat: remove todos * fix: update to remove console and update comment * feat: get swap tests working * feat: get all swap tests working * fix: update for three assets in logic * feat: all tests passing * fix: rm commented out test * feat: add preview swap tests * feat: move logic out of single use internal and use conversion rate everywhere * feat: move divRoundUp out of single use internal * feat: add full coverage for conversion tests * feat: add more preview cases * feat: refactor PSM to use three assets * fix: rm comment * feat: add interface, natspec, events, referral code, tests passing * fix: update to rm consolegp * feat: add events testing * feat: make precisions internal and add state var natspec * feat: finish natspec * feat: add readme * feat: add referral code note * fix: update constructor test * fix: update links * fix: reformatting * fix: update testing section * fix: improve overview * feat: add emojis * feat: remove all share burn logic, get all non inflation attack tests to pass * fix: cleanup diff * fix: update to use initial deposit instead of burn * feat: add readme section explaining attack * fix: minimize diff * fix: address bartek comments * feat: update all tests to work with new interfaces * feat: add deposit failure mode tests * feat: update to add assertions for return in deposit * feat: add withdraw failure tests * feat: update to address comments outside sharesToBurn * feat: update inflation attack test and readme * fix: update readme * feat: update test to constrain deposit/withdraw * feat: update to add both cases * feat: update per review * feat: update to use underscore bound, fix test * fix: typo * feat: add overrides, remove referrals, update referral type * fix: update expect emit * feat: update name and remove todos * feat: move files and set up structure * feat: update to rename files, contracts, and errors * fix: rm dup file, update toml * feat: get deposits working * chore: refactor into proper inheritance structure * feat: get all functions working with reverts * feat: update conversion * feat: get swaps working without reverts * feat: add fully working deposit/withdraw/swaps, invariant_B failing * ci: update for ci * fix: update name * chore: rm basly cased file * chore: re add * fix: re add invariant * ci: experiment with 2 million total calls * ci: add show progress flag * fix: move file back * ci: update verbosity * ci: add PR profile * fix: rm redundant files * feat: update from review changes * feat: add afterInvariant hook * fix: update invariant * fix: add fuzz failure * chore: rm indexing comment * feat: refactor structure * feat: both invariants working * fix: update comment * fix: update toml * fix: rm redundant files from merge * fix: update tolerances * feat: update to add seeding as part of invariants * fix: formatting * fix: review changes * fix: rm constant --- foundry.toml | 1 + test/invariant/Invariants.t.sol | 213 ++++++++++++++++++-- test/invariant/handlers/LpHandler.sol | 8 +- test/invariant/handlers/SwapperHandler.sol | 2 +- test/invariant/handlers/TransferHandler.sol | 38 ++++ 5 files changed, 237 insertions(+), 25 deletions(-) create mode 100644 test/invariant/handlers/TransferHandler.sol diff --git a/foundry.toml b/foundry.toml index d92768d..16bcba3 100644 --- a/foundry.toml +++ b/foundry.toml @@ -12,6 +12,7 @@ runs = 1000 [invariant] runs = 20 depth = 1000 +shrink_run_limit = 0 [profile.pr.invariant] runs = 200 diff --git a/test/invariant/Invariants.t.sol b/test/invariant/Invariants.t.sol index c2d7013..edfb6ce 100644 --- a/test/invariant/Invariants.t.sol +++ b/test/invariant/Invariants.t.sol @@ -5,36 +5,33 @@ import "forge-std/Test.sol"; import { PSMTestBase } from "test/PSMTestBase.sol"; -import { LpHandler } from "test/invariant/handlers/LpHandler.sol"; -import { SwapperHandler } from "test/invariant/handlers/SwapperHandler.sol"; +import { LpHandler } from "test/invariant/handlers/LpHandler.sol"; +import { SwapperHandler } from "test/invariant/handlers/SwapperHandler.sol"; +import { TransferHandler } from "test/invariant/handlers/TransferHandler.sol"; -contract PSMInvariantTests is PSMTestBase { +abstract contract PSMInvariantTestBase is PSMTestBase { - LpHandler public lpHandler; - SwapperHandler public swapperHandler; + LpHandler public lpHandler; + SwapperHandler public swapperHandler; + TransferHandler public transferHandler; address BURN_ADDRESS = makeAddr("burn-address"); // NOTE [CRITICAL]: All invariant tests are operating under the assumption that the initial seed // deposit of 1e18 shares has been made. This is a key requirement and // assumption for all invariant tests. - function setUp() public override { + function setUp() public virtual override { super.setUp(); // Seed the pool with 1e18 shares (1e18 of value) _deposit(address(dai), BURN_ADDRESS, 1e18); - - lpHandler = new LpHandler(psm, dai, usdc, sDai, 3); - swapperHandler = new SwapperHandler(psm, dai, usdc, sDai, 3); - - // TODO: Add rate updates - rateProvider.__setConversionRate(1.25e27); - - targetContract(address(lpHandler)); - targetContract(address(swapperHandler)); } - function invariant_A() public view { + /**********************************************************************************************/ + /*** Invariant assertion functions ***/ + /**********************************************************************************************/ + + function _checkInvariant_A() public view { assertEq( psm.shares(address(lpHandler.lps(0))) + psm.shares(address(lpHandler.lps(1))) + @@ -44,7 +41,7 @@ contract PSMInvariantTests is PSMTestBase { ); } - function invariant_B() public view { + function _checkInvariant_B() public view { assertApproxEqAbs( psm.getPsmTotalValue(), psm.convertToAssetValue(psm.totalShares()), @@ -52,7 +49,7 @@ contract PSMInvariantTests is PSMTestBase { ); } - function invariant_C() public view { + function _checkInvariant_C() public view { assertApproxEqAbs( psm.convertToAssetValue(psm.shares(address(lpHandler.lps(0)))) + psm.convertToAssetValue(psm.shares(address(lpHandler.lps(1)))) + @@ -63,7 +60,11 @@ contract PSMInvariantTests is PSMTestBase { ); } - function invariant_logs() public view { + /**********************************************************************************************/ + /*** Helper functions ***/ + /**********************************************************************************************/ + + function _logHandlerCallCounts() public view { console.log("depositCount ", lpHandler.depositCount()); console.log("withdrawCount ", lpHandler.withdrawCount()); console.log("swapCount ", swapperHandler.swapCount()); @@ -77,4 +78,178 @@ contract PSMInvariantTests is PSMTestBase { ); } + function _getLpTokenValue(address lp) internal view returns (uint256) { + uint256 daiValue = dai.balanceOf(lp); + uint256 usdcValue = usdc.balanceOf(lp) * 1e12; + uint256 sDaiValue = sDai.balanceOf(lp) * rateProvider.getConversionRate() / 1e27; + + return daiValue + usdcValue + sDaiValue; + } + + /**********************************************************************************************/ + /*** After invariant hook functions ***/ + /**********************************************************************************************/ + + function _withdrawAllPositions() public { + address lp0 = lpHandler.lps(0); + address lp1 = lpHandler.lps(1); + address lp2 = lpHandler.lps(2); + + // Get value of each LPs current deposits. + uint256 lp0DepositsValue = psm.convertToAssetValue(psm.shares(lp0)); + uint256 lp1DepositsValue = psm.convertToAssetValue(psm.shares(lp1)); + uint256 lp2DepositsValue = psm.convertToAssetValue(psm.shares(lp2)); + + // Get value of each LPs token holdings from previous withdrawals. + uint256 lp0WithdrawsValue = _getLpTokenValue(lp0); + uint256 lp1WithdrawsValue = _getLpTokenValue(lp1); + uint256 lp2WithdrawsValue = _getLpTokenValue(lp2); + + uint256 psmTotalValue = psm.getPsmTotalValue(); + + uint256 startingSeedValue = psm.convertToAssetValue(1e18); + + // Liquidity is unknown so withdraw all assets for all users to empty PSM. + _withdraw(address(dai), lp0, type(uint256).max); + _withdraw(address(usdc), lp0, type(uint256).max); + _withdraw(address(sDai), lp0, type(uint256).max); + + _withdraw(address(dai), lp1, type(uint256).max); + _withdraw(address(usdc), lp1, type(uint256).max); + _withdraw(address(sDai), lp1, type(uint256).max); + + _withdraw(address(dai), lp2, type(uint256).max); + _withdraw(address(usdc), lp2, type(uint256).max); + _withdraw(address(sDai), lp2, type(uint256).max); + + // All funds are completely withdrawn. + assertEq(psm.shares(lp0), 0); + assertEq(psm.shares(lp1), 0); + assertEq(psm.shares(lp2), 0); + + uint256 seedValue = psm.convertToAssetValue(1e18); + + // PSM is empty (besides seed amount). + assertEq(psm.totalShares(), 1e18); + assertEq(psm.getPsmTotalValue(), seedValue); + + // Tokens held by LPs are equal to the sum of their previous balance + // plus the amount of value originally represented in the PSM's shares. + // There can be rounding here because of share burning up to 1e12 when withdrawing USDC. + // It should be noted that LP2 here has a rounding error of 2e12 since both LP0 and LP1 + // could have rounding errors that accumulate to LP2. + assertApproxEqAbs(_getLpTokenValue(lp0), lp0DepositsValue + lp0WithdrawsValue, 1e12); + assertApproxEqAbs(_getLpTokenValue(lp1), lp1DepositsValue + lp1WithdrawsValue, 1e12); + assertApproxEqAbs(_getLpTokenValue(lp2), lp2DepositsValue + lp2WithdrawsValue, 2e12); + + // All rounding errors from LPs can accrue to the burn address after withdrawals are made. + assertApproxEqAbs(seedValue, startingSeedValue, 3e12); + + // Current value of all LPs' token holdings. + uint256 sumLpValue = _getLpTokenValue(lp0) + _getLpTokenValue(lp1) + _getLpTokenValue(lp2); + + // Total amount just withdrawn from the PSM. + uint256 totalWithdrawals + = sumLpValue - (lp0WithdrawsValue + lp1WithdrawsValue + lp2WithdrawsValue); + + // Assert that all funds were withdrawn equals the original value of the PSM minus the + // 1e18 share seed deposit. + assertApproxEqAbs(totalWithdrawals, psmTotalValue - seedValue, 2); + + // Get the starting sum of all LPs' deposits and withdrawals. + uint256 sumStartingValue = + (lp0DepositsValue + lp1DepositsValue + lp2DepositsValue) + + (lp0WithdrawsValue + lp1WithdrawsValue + lp2WithdrawsValue); + + // Assert that the sum of all LPs' deposits and withdrawals equals + // the sum of all LPs' resulting token holdings. Rounding errors are accumulated to the + // burn address. + assertApproxEqAbs(sumLpValue, sumStartingValue, seedValue - startingSeedValue + 2); + + // NOTE: Below logic is not realistic, shown to demonstrate precision. + + _withdraw(address(dai), BURN_ADDRESS, type(uint256).max); + _withdraw(address(usdc), BURN_ADDRESS, type(uint256).max); + _withdraw(address(sDai), BURN_ADDRESS, type(uint256).max); + + // When all funds are completely withdrawn, the sum of all funds withdrawn is equal to the + // sum of value of all LPs including the burn address. All rounding errors get reduced to + // a few wei. + assertApproxEqAbs( + sumLpValue + _getLpTokenValue(BURN_ADDRESS), + sumStartingValue + startingSeedValue, + 5 + ); + + // All funds can always be withdrawn completely. + assertEq(psm.totalShares(), 0); + assertEq(psm.getPsmTotalValue(), 0); + } + +} + +contract PSMInvariants_ConstantRate_NoTransfer is PSMInvariantTestBase { + + function setUp() public override { + super.setUp(); + + lpHandler = new LpHandler(psm, dai, usdc, sDai, 3); + swapperHandler = new SwapperHandler(psm, dai, usdc, sDai, 3); + + rateProvider.__setConversionRate(1.25e27); + + targetContract(address(lpHandler)); + targetContract(address(swapperHandler)); + } + + function invariant_A() public view { + _checkInvariant_A(); + } + + function invariant_B() public view { + _checkInvariant_B(); + } + + function invariant_C() public view { + _checkInvariant_C(); + } + + function afterInvariant() public { + _withdrawAllPositions(); + } + +} + +contract PSMInvariants_ConstantRate_WithTransfers is PSMInvariantTestBase { + + function setUp() public override { + super.setUp(); + + lpHandler = new LpHandler(psm, dai, usdc, sDai, 3); + swapperHandler = new SwapperHandler(psm, dai, usdc, sDai, 3); + transferHandler = new TransferHandler(psm, dai, usdc, sDai); + + rateProvider.__setConversionRate(1.25e27); + + targetContract(address(lpHandler)); + targetContract(address(swapperHandler)); + targetContract(address(transferHandler)); + } + + function invariant_A() public view { + _checkInvariant_A(); + } + + function invariant_B() public view { + _checkInvariant_B(); + } + + function invariant_C() public view { + _checkInvariant_C(); + } + + function afterInvariant() public { + _withdrawAllPositions(); + } + } diff --git a/test/invariant/handlers/LpHandler.sol b/test/invariant/handlers/LpHandler.sol index f321e2a..fcbb068 100644 --- a/test/invariant/handlers/LpHandler.sol +++ b/test/invariant/handlers/LpHandler.sol @@ -14,8 +14,6 @@ contract LpHandler is HandlerBase { uint256 public depositCount; uint256 public withdrawCount; - uint256 public constant TRILLION = 1e12; - constructor( PSM3 psm_, MockERC20 asset0, @@ -24,7 +22,7 @@ contract LpHandler is HandlerBase { uint256 lpCount ) HandlerBase(psm_, asset0, asset1, asset2) { for (uint256 i = 0; i < lpCount; i++) { - lps.push(makeAddr(string(abi.encodePacked("lp-", i)))); + lps.push(makeAddr(string(abi.encodePacked("lp-", vm.toString(i))))); } } @@ -36,7 +34,7 @@ contract LpHandler is HandlerBase { MockERC20 asset = _getAsset(assetSeed); address lp = _getLP(lpSeed); - amount = _bound(amount, 1, TRILLION * 10 ** asset.decimals()); + amount = _bound(amount, 1, 1e12 * 10 ** asset.decimals()); vm.startPrank(lp); asset.mint(lp, amount); @@ -51,7 +49,7 @@ contract LpHandler is HandlerBase { MockERC20 asset = _getAsset(assetSeed); address lp = _getLP(lpSeed); - amount = _bound(amount, 1, TRILLION * 10 ** asset.decimals()); + amount = _bound(amount, 1, 1e12 * 10 ** asset.decimals()); vm.prank(lp); psm.withdraw(address(asset), lp, amount); diff --git a/test/invariant/handlers/SwapperHandler.sol b/test/invariant/handlers/SwapperHandler.sol index c0e2bb1..5fcbfeb 100644 --- a/test/invariant/handlers/SwapperHandler.sol +++ b/test/invariant/handlers/SwapperHandler.sol @@ -22,7 +22,7 @@ contract SwapperHandler is HandlerBase { uint256 lpCount ) HandlerBase(psm_, asset0, asset1, asset2) { for (uint256 i = 0; i < lpCount; i++) { - swappers.push(makeAddr(string(abi.encodePacked("swapper-", i)))); + swappers.push(makeAddr(string(abi.encodePacked("swapper-", vm.toString(i))))); } } diff --git a/test/invariant/handlers/TransferHandler.sol b/test/invariant/handlers/TransferHandler.sol new file mode 100644 index 0000000..fe579e6 --- /dev/null +++ b/test/invariant/handlers/TransferHandler.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.13; + +import { MockERC20 } from "erc20-helpers/MockERC20.sol"; + +import { HandlerBase } from "test/invariant/handlers/HandlerBase.sol"; + +import { PSM3 } from "src/PSM3.sol"; + +contract TransferHandler is HandlerBase { + + uint256 public transferCount; + + constructor( + PSM3 psm_, + MockERC20 asset0, + MockERC20 asset1, + MockERC20 asset2 + ) HandlerBase(psm_, asset0, asset1, asset2) {} + + function transfer(uint256 assetSeed, string memory senderSeed, uint256 amount) external { + MockERC20 asset = _getAsset(assetSeed); + address sender = makeAddr(senderSeed); + + // Bounding to 10 million here because 1 trillion introduces unrealistic conditions with + // large rounding errors. Would rather keep tolerances smaller with a lower upper bound + // on transfer amounts. + amount = _bound(amount, 1, 10_000_000 * 10 ** asset.decimals()); + + asset.mint(sender, amount); + + vm.prank(sender); + asset.transfer(address(psm), amount); + + transferCount += 1; + } + +} From fafd16ea2a8427682e3ca59a068037206ef25bf5 Mon Sep 17 00:00:00 2001 From: Lucas Manuel Date: Wed, 3 Jul 2024 06:59:39 -0400 Subject: [PATCH 7/9] test: Add rate setting logic (SC-483) (#15) * feat: first test working * feat: use larger numbers: * feat: test with initial burn amount passing * feat: update tests to work with updated burn logic, move conversion functions around and use previews * feat: remove todos * fix: update to remove console and update comment * feat: get swap tests working * feat: get all swap tests working * fix: update for three assets in logic * feat: all tests passing * fix: rm commented out test * feat: add preview swap tests * feat: move logic out of single use internal and use conversion rate everywhere * feat: move divRoundUp out of single use internal * feat: add full coverage for conversion tests * feat: add more preview cases * feat: refactor PSM to use three assets * fix: rm comment * feat: add interface, natspec, events, referral code, tests passing * fix: update to rm consolegp * feat: add events testing * feat: make precisions internal and add state var natspec * feat: finish natspec * feat: add readme * feat: add referral code note * fix: update constructor test * fix: update links * fix: reformatting * fix: update testing section * fix: improve overview * feat: add emojis * feat: remove all share burn logic, get all non inflation attack tests to pass * fix: cleanup diff * fix: update to use initial deposit instead of burn * feat: add readme section explaining attack * fix: minimize diff * fix: address bartek comments * feat: update all tests to work with new interfaces * feat: add deposit failure mode tests * feat: update to add assertions for return in deposit * feat: add withdraw failure tests * feat: update to address comments outside sharesToBurn * feat: update inflation attack test and readme * fix: update readme * feat: update test to constrain deposit/withdraw * feat: update to add both cases * feat: update per review * feat: update to use underscore bound, fix test * fix: typo * feat: add overrides, remove referrals, update referral type * fix: update expect emit * feat: update name and remove todos * feat: move files and set up structure * feat: update to rename files, contracts, and errors * fix: rm dup file, update toml * feat: get deposits working * chore: refactor into proper inheritance structure * feat: get all functions working with reverts * feat: update conversion * feat: get swaps working without reverts * feat: add fully working deposit/withdraw/swaps, invariant_B failing * ci: update for ci * fix: update name * chore: rm basly cased file * chore: re add * fix: re add invariant * ci: experiment with 2 million total calls * ci: add show progress flag * fix: move file back * ci: update verbosity * ci: add PR profile * fix: rm redundant files * feat: update from review changes * feat: add afterInvariant hook * fix: update invariant * fix: add fuzz failure * chore: rm indexing comment * feat: refactor structure * feat: both invariants working * fix: update comment * feat: add rate setting logic * fix: update toml * fix: rm redundant files from merge * fix: update tolerances * feat: update to add seeding as part of invariants * fix: update toml * fix: rm redundant files from merge * fix: update tolerances * fix: rm invariant logs * fix: update tolerance * fix: formatting * fix: update test name --- test/invariant/Invariants.t.sol | 88 ++++++++++++++++--- test/invariant/handlers/RateSetterHandler.sol | 30 +++++++ 2 files changed, 107 insertions(+), 11 deletions(-) create mode 100644 test/invariant/handlers/RateSetterHandler.sol diff --git a/test/invariant/Invariants.t.sol b/test/invariant/Invariants.t.sol index edfb6ce..c39957f 100644 --- a/test/invariant/Invariants.t.sol +++ b/test/invariant/Invariants.t.sol @@ -5,15 +5,17 @@ import "forge-std/Test.sol"; import { PSMTestBase } from "test/PSMTestBase.sol"; -import { LpHandler } from "test/invariant/handlers/LpHandler.sol"; -import { SwapperHandler } from "test/invariant/handlers/SwapperHandler.sol"; -import { TransferHandler } from "test/invariant/handlers/TransferHandler.sol"; +import { LpHandler } from "test/invariant/handlers/LpHandler.sol"; +import { RateSetterHandler } from "test/invariant/handlers/RateSetterHandler.sol"; +import { SwapperHandler } from "test/invariant/handlers/SwapperHandler.sol"; +import { TransferHandler } from "test/invariant/handlers/TransferHandler.sol"; abstract contract PSMInvariantTestBase is PSMTestBase { - LpHandler public lpHandler; - SwapperHandler public swapperHandler; - TransferHandler public transferHandler; + LpHandler public lpHandler; + RateSetterHandler public rateSetterHandler; + SwapperHandler public swapperHandler; + TransferHandler public transferHandler; address BURN_ADDRESS = makeAddr("burn-address"); @@ -45,7 +47,7 @@ abstract contract PSMInvariantTestBase is PSMTestBase { assertApproxEqAbs( psm.getPsmTotalValue(), psm.convertToAssetValue(psm.totalShares()), - 2 + 3 ); } @@ -69,12 +71,14 @@ abstract contract PSMInvariantTestBase is PSMTestBase { console.log("withdrawCount ", lpHandler.withdrawCount()); console.log("swapCount ", swapperHandler.swapCount()); console.log("zeroBalanceCount", swapperHandler.zeroBalanceCount()); + console.log("setRateCount ", rateSetterHandler.setRateCount()); console.log( "sum ", lpHandler.depositCount() + lpHandler.withdrawCount() + swapperHandler.swapCount() + - swapperHandler.zeroBalanceCount() + swapperHandler.zeroBalanceCount() + + rateSetterHandler.setRateCount() ); } @@ -196,8 +200,6 @@ contract PSMInvariants_ConstantRate_NoTransfer is PSMInvariantTestBase { lpHandler = new LpHandler(psm, dai, usdc, sDai, 3); swapperHandler = new SwapperHandler(psm, dai, usdc, sDai, 3); - rateProvider.__setConversionRate(1.25e27); - targetContract(address(lpHandler)); targetContract(address(swapperHandler)); } @@ -229,9 +231,73 @@ contract PSMInvariants_ConstantRate_WithTransfers is PSMInvariantTestBase { swapperHandler = new SwapperHandler(psm, dai, usdc, sDai, 3); transferHandler = new TransferHandler(psm, dai, usdc, sDai); - rateProvider.__setConversionRate(1.25e27); + targetContract(address(lpHandler)); + targetContract(address(swapperHandler)); + targetContract(address(transferHandler)); + } + + function invariant_A() public view { + _checkInvariant_A(); + } + + function invariant_B() public view { + _checkInvariant_B(); + } + + function invariant_C() public view { + _checkInvariant_C(); + } + + function afterInvariant() public { + _withdrawAllPositions(); + } + +} + +contract PSMInvariants_RateSetting_NoTransfer is PSMInvariantTestBase { + + function setUp() public override { + super.setUp(); + + lpHandler = new LpHandler(psm, dai, usdc, sDai, 3); + rateSetterHandler = new RateSetterHandler(rateProvider, 1.25e27); + swapperHandler = new SwapperHandler(psm, dai, usdc, sDai, 3); + + targetContract(address(lpHandler)); + targetContract(address(rateSetterHandler)); + targetContract(address(swapperHandler)); + } + + function invariant_A() public view { + _checkInvariant_A(); + } + + function invariant_B() public view { + _checkInvariant_B(); + } + + function invariant_C() public view { + _checkInvariant_C(); + } + + function afterInvariant() public { + _withdrawAllPositions(); + } + +} + +contract PSMInvariants_RateSetting_WithTransfers is PSMInvariantTestBase { + + function setUp() public override { + super.setUp(); + + lpHandler = new LpHandler(psm, dai, usdc, sDai, 3); + rateSetterHandler = new RateSetterHandler(rateProvider, 1.25e27); + swapperHandler = new SwapperHandler(psm, dai, usdc, sDai, 3); + transferHandler = new TransferHandler(psm, dai, usdc, sDai); targetContract(address(lpHandler)); + targetContract(address(rateSetterHandler)); targetContract(address(swapperHandler)); targetContract(address(transferHandler)); } diff --git a/test/invariant/handlers/RateSetterHandler.sol b/test/invariant/handlers/RateSetterHandler.sol new file mode 100644 index 0000000..ffedfd1 --- /dev/null +++ b/test/invariant/handlers/RateSetterHandler.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.13; + +import { StdUtils } from "forge-std/StdUtils.sol"; + +import { MockRateProvider } from "test/mocks/MockRateProvider.sol"; + +contract RateSetterHandler is StdUtils { + + uint256 public rate; + + MockRateProvider public rateProvider; + + uint256 public setRateCount; + + constructor(MockRateProvider rateProvider_, uint256 initialRate) { + rateProvider = rateProvider_; + rate = initialRate; + } + + function setRate(uint256 rateIncrease) external { + // Increase the rate by up to 100% + rate += _bound(rateIncrease, 0, 1e27); + + rateProvider.__setConversionRate(rate); + + setRateCount++; + } + +} From ae6c47b1bd3d2108e133bdcc4274319184906e96 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Wed, 3 Jul 2024 08:38:13 -0400 Subject: [PATCH 8/9] chore: first cleanup --- test/PSMTestBase.sol | 6 ++- test/invariant/Invariants.t.sol | 53 +++++++++++++++++- .../handlers/TimeBasedRateHandler.sol | 1 - test/unit/Conversions.t.sol | 54 +++++++++---------- test/unit/Deposit.t.sol | 4 +- test/unit/Getters.t.sol | 14 ++--- test/unit/Previews.t.sol | 8 +-- test/unit/Rounding.t.sol | 6 +-- test/unit/Swaps.t.sol | 8 +-- test/unit/Withdraw.t.sol | 4 +- 10 files changed, 104 insertions(+), 54 deletions(-) diff --git a/test/PSMTestBase.sol b/test/PSMTestBase.sol index 2b7ae2b..c599c39 100644 --- a/test/PSMTestBase.sol +++ b/test/PSMTestBase.sol @@ -20,7 +20,9 @@ contract PSMTestBase is Test { MockERC20 public usdc; MockERC20 public sDai; - IRateProviderLike public rateProvider; + IRateProviderLike public rateProvider; // Can be overridden by dsrOracle using same interface + + MockRateProvider public mockRateProvider; // Interface used for mocking modifier assertAtomicPsmValueDoesNotChange { uint256 beforeValue = _getPsmValue(); @@ -38,7 +40,7 @@ contract PSMTestBase is Test { usdc = new MockERC20("usdc", "usdc", 6); sDai = new MockERC20("sDai", "sDai", 18); - MockRateProvider mockRateProvider = new MockRateProvider(); + mockRateProvider = new MockRateProvider(); // NOTE: Using 1.25 for easy two way conversions mockRateProvider.__setConversionRate(1.25e27); diff --git a/test/invariant/Invariants.t.sol b/test/invariant/Invariants.t.sol index 1b02dd4..e3fb5d7 100644 --- a/test/invariant/Invariants.t.sol +++ b/test/invariant/Invariants.t.sol @@ -55,7 +55,7 @@ abstract contract PSMInvariantTestBase is PSMTestBase { assertApproxEqAbs( psm.getPsmTotalValue(), psm.convertToAssetValue(psm.totalShares()), - 3 + 4 ); } @@ -377,4 +377,53 @@ contract PSMInvariants_TimeBasedRateSetting_NoTransfer is PSMInvariantTestBase { } -// TODO: Add transfers (check other PR) +contract PSMInvariants_TimeBasedRateSetting_WithTransfers is PSMInvariantTestBase { + + function setUp() public override { + super.setUp(); + + DSRAuthOracle dsrOracle = new DSRAuthOracle(); + + // Redeploy PSM with new rate provider + psm = new PSM3(address(dai), address(usdc), address(sDai), address(dsrOracle)); + + // Seed the new PSM with 1e18 shares (1e18 of value) + _deposit(address(dai), BURN_ADDRESS, 1e18); + + lpHandler = new LpHandler(psm, dai, usdc, sDai, 3); + swapperHandler = new SwapperHandler(psm, dai, usdc, sDai, 3); + timeBasedRateHandler = new TimeBasedRateHandler(dsrOracle); + transferHandler = new TransferHandler(psm, dai, usdc, sDai); + + // Handler acts in the same way as a receiver on L2, so add as a data provider to the + // oracle. + dsrOracle.grantRole(dsrOracle.DATA_PROVIDER_ROLE(), address(timeBasedRateHandler)); + + rateProvider = IRateProviderLike(address(dsrOracle)); + + // Manually set initial values for the oracle through the handler to start + timeBasedRateHandler.setPotData(1e27, 1e27, block.timestamp); + + targetContract(address(lpHandler)); + targetContract(address(swapperHandler)); + targetContract(address(timeBasedRateHandler)); + targetContract(address(transferHandler)); + } + + function invariant_A_test() public view { + _checkInvariant_A(); + } + + function invariant_B() public view { + _checkInvariant_B(); + } + + function invariant_C() public view { + _checkInvariant_C(); + } + + function afterInvariant() public { + _withdrawAllPositions(); + } + +} diff --git a/test/invariant/handlers/TimeBasedRateHandler.sol b/test/invariant/handlers/TimeBasedRateHandler.sol index c23da27..e97298a 100644 --- a/test/invariant/handlers/TimeBasedRateHandler.sol +++ b/test/invariant/handlers/TimeBasedRateHandler.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity ^0.8.13; -import { console } from "forge-std/console.sol"; import { StdCheats } from "forge-std/StdCheats.sol"; import { StdUtils } from "forge-std/StdUtils.sol"; diff --git a/test/unit/Conversions.t.sol b/test/unit/Conversions.t.sol index ab09279..1123c9e 100644 --- a/test/unit/Conversions.t.sol +++ b/test/unit/Conversions.t.sol @@ -94,7 +94,7 @@ contract PSMConvertToAssetsTests is PSMTestBase { conversionRate = _bound(conversionRate, 0.0001e27, 1000e27); amount = _bound(amount, 0, SDAI_TOKEN_MAX); - MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); assertEq(psm.convertToAssets(address(sDai), amount), amount * 1e27 / conversionRate); } @@ -114,7 +114,7 @@ contract PSMConvertToAssetValueTests is PSMConversionTestBase { assertEq(psm.convertToAssetValue(1e18), 1e18); - MockRateProvider(address(rateProvider)).__setConversionRate(2e27); + 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 @@ -129,7 +129,7 @@ contract PSMConvertToAssetValueTests is PSMConversionTestBase { ) public { - MockRateProvider(address(rateProvider)).__setConversionRate(1e27); // Start lower than 1.25 for this test + mockRateProvider.__setConversionRate(1e27); // Start lower than 1.25 for this test FuzzVars memory vars = _setUpConversionFuzzTest( 1e27, @@ -146,7 +146,7 @@ contract PSMConvertToAssetValueTests is PSMConversionTestBase { // 1:1 between shares and dollar value assertEq(psm.convertToAssetValue(vars.expectedShares), initialValue); - MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); uint256 newValue = vars.daiAmount + vars.usdcAmount * 1e12 + vars.sDaiAmount * conversionRate / 1e27; @@ -165,7 +165,7 @@ contract PSMConvertToAssetValueTests is PSMConversionTestBase { ) public { - MockRateProvider(address(rateProvider)).__setConversionRate(2e27); // Start higher than 1.25 for this test + mockRateProvider.__setConversionRate(2e27); // Start higher than 1.25 for this test FuzzVars memory vars = _setUpConversionFuzzTest( 2e27, @@ -182,7 +182,7 @@ contract PSMConvertToAssetValueTests is PSMConversionTestBase { // 1:1 between shares and dollar value assertEq(psm.convertToAssetValue(vars.expectedShares), initialValue); - MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); uint256 newValue = vars.daiAmount + vars.usdcAmount * 1e12 + vars.sDaiAmount * conversionRate / 1e27; @@ -234,7 +234,7 @@ contract PSMConvertToSharesTests is PSMConversionTestBase { // 80 sDAI now worth $120, 200 shares in pool with $220 of value // Each share should be worth $1.10. - MockRateProvider(address(rateProvider)).__setConversionRate(1.5e27); + mockRateProvider.__setConversionRate(1.5e27); assertEq(psm.convertToShares(10), 9); assertEq(psm.convertToShares(11), 10); @@ -253,7 +253,7 @@ contract PSMConvertToSharesTests is PSMConversionTestBase { ) public { - MockRateProvider(address(rateProvider)).__setConversionRate(1e27); // Start lower than 1.25 for this test + mockRateProvider.__setConversionRate(1e27); // Start lower than 1.25 for this test FuzzVars memory vars = _setUpConversionFuzzTest( 1e27, @@ -270,7 +270,7 @@ contract PSMConvertToSharesTests is PSMConversionTestBase { // 1:1 between shares and dollar value assertEq(psm.convertToShares(initialValue), vars.expectedShares); - MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); uint256 newValue = vars.daiAmount + vars.usdcAmount * 1e12 + vars.sDaiAmount * conversionRate / 1e27; @@ -289,7 +289,7 @@ contract PSMConvertToSharesTests is PSMConversionTestBase { ) public { - MockRateProvider(address(rateProvider)).__setConversionRate(2e27); // Start higher than 1.25 for this test + mockRateProvider.__setConversionRate(2e27); // Start higher than 1.25 for this test FuzzVars memory vars = _setUpConversionFuzzTest( 2e27, @@ -306,7 +306,7 @@ contract PSMConvertToSharesTests is PSMConversionTestBase { // 1:1 between shares and dollar value assertEq(psm.convertToShares(initialValue), vars.expectedShares); - MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); uint256 newValue = vars.daiAmount + vars.usdcAmount * 1e12 + vars.sDaiAmount * conversionRate / 1e27; @@ -380,7 +380,7 @@ contract PSMConvertToSharesWithDaiTests is PSMConversionTestBase { // 80 sDAI now worth $120, 200 shares in pool with $220 of value // Each share should be worth $1.10. - MockRateProvider(address(rateProvider)).__setConversionRate(1.5e27); + mockRateProvider.__setConversionRate(1.5e27); assertEq(psm.convertToShares(address(dai), 10), 9); assertEq(psm.convertToShares(address(dai), 11), 10); @@ -402,7 +402,7 @@ contract PSMConvertToSharesWithDaiTests is PSMConversionTestBase { ) public { - MockRateProvider(address(rateProvider)).__setConversionRate(1e27); // Start lower than 1.25 for this test + mockRateProvider.__setConversionRate(1e27); // Start lower than 1.25 for this test FuzzVars memory vars = _setUpConversionFuzzTest( 1e27, @@ -419,7 +419,7 @@ contract PSMConvertToSharesWithDaiTests is PSMConversionTestBase { // 1:1 between shares and dollar value assertEq(psm.convertToShares(address(dai), initialValue), vars.expectedShares); - MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); uint256 newValue = vars.daiAmount + vars.usdcAmount * 1e12 + vars.sDaiAmount * conversionRate / 1e27; @@ -438,7 +438,7 @@ contract PSMConvertToSharesWithDaiTests is PSMConversionTestBase { ) public { - MockRateProvider(address(rateProvider)).__setConversionRate(2e27); // Start higher than 1.25 for this test + mockRateProvider.__setConversionRate(2e27); // Start higher than 1.25 for this test FuzzVars memory vars = _setUpConversionFuzzTest( 2e27, @@ -455,7 +455,7 @@ contract PSMConvertToSharesWithDaiTests is PSMConversionTestBase { // 1:1 between shares and dollar value assertEq(psm.convertToShares(address(dai), initialValue), vars.expectedShares); - MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); uint256 newValue = vars.daiAmount + vars.usdcAmount * 1e12 + vars.sDaiAmount * conversionRate / 1e27; @@ -520,7 +520,7 @@ contract PSMConvertToSharesWithUsdcTests is PSMConversionTestBase { // 80 sDAI now worth $120, 200 shares in pool with $220 of value // Each share should be worth $1.10. - MockRateProvider(address(rateProvider)).__setConversionRate(1.5e27); + mockRateProvider.__setConversionRate(1.5e27); assertEq(psm.convertToShares(address(usdc), 10), 9.090909090909e12); assertEq(psm.convertToShares(address(usdc), 11), 10e12); @@ -539,7 +539,7 @@ contract PSMConvertToSharesWithUsdcTests is PSMConversionTestBase { ) public { - MockRateProvider(address(rateProvider)).__setConversionRate(1e27); // Start lower than 1.25 for this test + mockRateProvider.__setConversionRate(1e27); // Start lower than 1.25 for this test FuzzVars memory vars = _setUpConversionFuzzTest( 1e27, @@ -560,7 +560,7 @@ contract PSMConvertToSharesWithUsdcTests is PSMConversionTestBase { vars.expectedShares / 1e12 * 1e12 ); - MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); uint256 newValue = vars.daiAmount + vars.usdcAmount * 1e12 + vars.sDaiAmount * conversionRate / 1e27; @@ -596,7 +596,7 @@ contract PSMConvertToSharesWithUsdcTests is PSMConversionTestBase { ) public { - MockRateProvider(address(rateProvider)).__setConversionRate(2e27); // Start higher than 1.25 for this test + mockRateProvider.__setConversionRate(2e27); // Start higher than 1.25 for this test FuzzVars memory vars = _setUpConversionFuzzTest( 2e27, @@ -617,7 +617,7 @@ contract PSMConvertToSharesWithUsdcTests is PSMConversionTestBase { vars.expectedShares / 1e12 * 1e12 ); - MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); uint256 newValue = vars.daiAmount + vars.usdcAmount * 1e12 + vars.sDaiAmount * conversionRate / 1e27; @@ -673,7 +673,7 @@ contract PSMConvertToSharesWithSDaiTests is PSMConversionTestBase { amount = _bound(amount, 1000, SDAI_TOKEN_MAX); conversionRate = _bound(conversionRate, 0.01e27, 1000e27); - MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); assertEq(psm.convertToShares(address(sDai), amount), amount * conversionRate / 1e27); } @@ -704,7 +704,7 @@ contract PSMConvertToSharesWithSDaiTests is PSMConversionTestBase { // 80 sDAI now worth $120, 200 shares in pool with $220 of value // Each share should be worth $1.10. Since 1 sDAI is now worth 1.5 USDC, 1 sDAI is worth // 1.50/1.10 = 1.3636... shares - MockRateProvider(address(rateProvider)).__setConversionRate(1.5e27); + mockRateProvider.__setConversionRate(1.5e27); // TODO: Reinvestigate this, interesting difference in rounding assertEq(psm.convertToShares(address(sDai), 1), 0); @@ -727,7 +727,7 @@ contract PSMConvertToSharesWithSDaiTests is PSMConversionTestBase { public { // NOTE: Not using 1e27 for this test because initialSDaiValue needs to be different - MockRateProvider(address(rateProvider)).__setConversionRate(1.1e27); // Start lower than 1.25 for this test + mockRateProvider.__setConversionRate(1.1e27); // Start lower than 1.25 for this test FuzzVars memory vars = _setUpConversionFuzzTest( 1.1e27, @@ -749,7 +749,7 @@ contract PSMConvertToSharesWithSDaiTests is PSMConversionTestBase { 1 ); - MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); uint256 newValue = vars.daiAmount + vars.usdcAmount * 1e12 + vars.sDaiAmount * conversionRate / 1e27; @@ -791,7 +791,7 @@ contract PSMConvertToSharesWithSDaiTests is PSMConversionTestBase { ) public { - MockRateProvider(address(rateProvider)).__setConversionRate(2e27); // Start higher than 1.25 for this test + mockRateProvider.__setConversionRate(2e27); // Start higher than 1.25 for this test FuzzVars memory vars = _setUpConversionFuzzTest( 2e27, @@ -813,7 +813,7 @@ contract PSMConvertToSharesWithSDaiTests is PSMConversionTestBase { 1 ); - MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); uint256 newValue = vars.daiAmount + vars.usdcAmount * 1e12 + vars.sDaiAmount * conversionRate / 1e27; diff --git a/test/unit/Deposit.t.sol b/test/unit/Deposit.t.sol index 2e3566f..32f56c6 100644 --- a/test/unit/Deposit.t.sol +++ b/test/unit/Deposit.t.sol @@ -279,7 +279,7 @@ contract PSMDepositTests is PSMTestBase { assertEq(psm.convertToAssetValue(psm.shares(receiver1)), 225e18); - MockRateProvider(address(rateProvider)).__setConversionRate(1.5e27); + mockRateProvider.__setConversionRate(1.5e27); // Total shares / (100 USDC + 150 sDAI value) uint256 expectedConversionRate = 225 * 1e18 / 250; @@ -375,7 +375,7 @@ contract PSMDepositTests is PSMTestBase { assertEq(psm.shares(user1), 0); assertEq(psm.shares(receiver1), receiver1Shares); - MockRateProvider(address(rateProvider)).__setConversionRate(newRate); + mockRateProvider.__setConversionRate(newRate); vm.startPrank(user2); diff --git a/test/unit/Getters.t.sol b/test/unit/Getters.t.sol index 11744cd..408d046 100644 --- a/test/unit/Getters.t.sol +++ b/test/unit/Getters.t.sol @@ -72,7 +72,7 @@ contract PSMHarnessTests is PSMTestBase { assertEq(psmHarness.getAsset2Value(3e18), 3.75e18); assertEq(psmHarness.getAsset2Value(4e18), 5e18); - MockRateProvider(address(rateProvider)).__setConversionRate(1.6e27); + mockRateProvider.__setConversionRate(1.6e27); assertEq(psmHarness.getAsset2Value(1), 1); assertEq(psmHarness.getAsset2Value(2), 3); @@ -84,7 +84,7 @@ contract PSMHarnessTests is PSMTestBase { assertEq(psmHarness.getAsset2Value(3e18), 4.8e18); assertEq(psmHarness.getAsset2Value(4e18), 6.4e18); - MockRateProvider(address(rateProvider)).__setConversionRate(0.8e27); + mockRateProvider.__setConversionRate(0.8e27); assertEq(psmHarness.getAsset2Value(1), 0); assertEq(psmHarness.getAsset2Value(2), 1); @@ -101,7 +101,7 @@ contract PSMHarnessTests is PSMTestBase { conversionRate = _bound(conversionRate, 0, 1000e27); amount = _bound(amount, 0, SDAI_TOKEN_MAX); - MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); assertEq(psmHarness.getAsset2Value(amount), amount * conversionRate / 1e27); } @@ -184,11 +184,11 @@ contract GetPsmTotalValueTests is PSMTestBase { assertEq(psm.getPsmTotalValue(), 3.25e18); - MockRateProvider(address(rateProvider)).__setConversionRate(1.5e27); + mockRateProvider.__setConversionRate(1.5e27); assertEq(psm.getPsmTotalValue(), 3.5e18); - MockRateProvider(address(rateProvider)).__setConversionRate(0.8e27); + mockRateProvider.__setConversionRate(0.8e27); assertEq(psm.getPsmTotalValue(), 2.8e18); } @@ -202,7 +202,7 @@ contract GetPsmTotalValueTests is PSMTestBase { assertEq(psm.getPsmTotalValue(), 3.25e18); - MockRateProvider(address(rateProvider)).__setConversionRate(1.5e27); + mockRateProvider.__setConversionRate(1.5e27); assertEq(psm.getPsmTotalValue(), 3.5e18); @@ -228,7 +228,7 @@ contract GetPsmTotalValueTests is PSMTestBase { usdc.mint(address(psm), usdcAmount); sDai.mint(address(psm), sDaiAmount); - MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); assertEq( psm.getPsmTotalValue(), diff --git a/test/unit/Previews.t.sol b/test/unit/Previews.t.sol index 74f5c02..2d7e2f0 100644 --- a/test/unit/Previews.t.sol +++ b/test/unit/Previews.t.sol @@ -61,7 +61,7 @@ contract PSMPreviewSwapDaiAssetInTests is PSMTestBase { amountIn = _bound(amountIn, 1, DAI_TOKEN_MAX); conversionRate = _bound(conversionRate, 0.0001e27, 1000e27); // 0.01% to 100,000% conversion rate - MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); uint256 amountOut = amountIn * 1e27 / conversionRate; @@ -94,7 +94,7 @@ contract PSMPreviewSwapUSDCAssetInTests is PSMTestBase { amountIn = _bound(amountIn, 1, USDC_TOKEN_MAX); conversionRate = _bound(conversionRate, 0.0001e27, 1000e27); // 0.01% to 100,000% conversion rate - MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); uint256 amountOut = amountIn * 1e27 / conversionRate * 1e12; @@ -115,7 +115,7 @@ contract PSMPreviewSwapSDaiAssetInTests is PSMTestBase { amountIn = _bound(amountIn, 1, SDAI_TOKEN_MAX); conversionRate = _bound(conversionRate, 0.0001e27, 1000e27); // 0.01% to 100,000% conversion rate - MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); uint256 amountOut = amountIn * conversionRate / 1e27; @@ -132,7 +132,7 @@ contract PSMPreviewSwapSDaiAssetInTests is PSMTestBase { amountIn = _bound(amountIn, 1, SDAI_TOKEN_MAX); conversionRate = _bound(conversionRate, 0.0001e27, 1000e27); // 0.01% to 100,000% conversion rate - MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); uint256 amountOut = amountIn * conversionRate / 1e27 / 1e12; diff --git a/test/unit/Rounding.t.sol b/test/unit/Rounding.t.sol index 725dcee..641c32f 100644 --- a/test/unit/Rounding.t.sol +++ b/test/unit/Rounding.t.sol @@ -20,7 +20,7 @@ contract RoundingTests is PSMTestBase { _deposit(address(usdc), address(this), USDC_TOKEN_MAX); // Set an exchange rate that will cause rounding - MockRateProvider(address(rateProvider)).__setConversionRate(1.25e27 * uint256(100) / 99); + mockRateProvider.__setConversionRate(1.25e27 * uint256(100) / 99); } function test_roundAgainstUser_dai() public { @@ -131,7 +131,7 @@ contract RoundingTests is PSMTestBase { amount1 = _bound(amount1, 1, tokenMax); amount2 = _bound(amount2, 1, tokenMax); - MockRateProvider(address(rateProvider)).__setConversionRate(rate1); + mockRateProvider.__setConversionRate(rate1); _deposit(address(asset), address(user1), amount1); @@ -144,7 +144,7 @@ contract RoundingTests is PSMTestBase { assertApproxEqAbs(asset.balanceOf(address(user1)), amount1, roundingTolerance); assertLe(asset.balanceOf(address(user1)), amount1); - MockRateProvider(address(rateProvider)).__setConversionRate(rate2); + mockRateProvider.__setConversionRate(rate2); _deposit(address(asset), address(user2), amount2); diff --git a/test/unit/Swaps.t.sol b/test/unit/Swaps.t.sol index a106069..67739d8 100644 --- a/test/unit/Swaps.t.sol +++ b/test/unit/Swaps.t.sol @@ -222,7 +222,7 @@ contract PSMSwapDaiAssetInTests is PSMSwapSuccessTestsBase { amountIn = _bound(amountIn, 1, DAI_TOKEN_MAX); conversionRate = _bound(conversionRate, 0.01e27, 100e27); // 1% to 10,000% conversion rate - MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); uint256 amountOut = amountIn * 1e27 / conversionRate; @@ -276,7 +276,7 @@ contract PSMSwapUsdcAssetInTests is PSMSwapSuccessTestsBase { amountIn = _bound(amountIn, 1, USDC_TOKEN_MAX); conversionRate = _bound(conversionRate, 0.01e27, 100e27); // 1% to 10,000% conversion rate - MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); uint256 amountOut = amountIn * 1e27 / conversionRate * 1e12; @@ -316,7 +316,7 @@ contract PSMSwapSDaiAssetInTests is PSMSwapSuccessTestsBase { amountIn = _bound(amountIn, 1, SDAI_TOKEN_MAX); conversionRate = _bound(conversionRate, 0.01e27, 100e27); // 1% to 10,000% conversion rate - MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); uint256 amountOut = amountIn * conversionRate / 1e27; @@ -336,7 +336,7 @@ contract PSMSwapSDaiAssetInTests is PSMSwapSuccessTestsBase { amountIn = _bound(amountIn, 1, SDAI_TOKEN_MAX); conversionRate = _bound(conversionRate, 0.01e27, 100e27); // 1% to 10,000% conversion rate - MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); uint256 amountOut = amountIn * conversionRate / 1e27 / 1e12; diff --git a/test/unit/Withdraw.t.sol b/test/unit/Withdraw.t.sol index 1a21805..e2a65ed 100644 --- a/test/unit/Withdraw.t.sol +++ b/test/unit/Withdraw.t.sol @@ -435,7 +435,7 @@ contract PSMWithdrawTests is PSMTestBase { assertEq(psm.convertToShares(1e18), 1e18); - MockRateProvider(address(rateProvider)).__setConversionRate(1.5e27); + mockRateProvider.__setConversionRate(1.5e27); // Total shares / (100 USDC + 150 sDAI value) uint256 expectedConversionRate = 225 * 1e18 / 250; @@ -525,7 +525,7 @@ contract PSMWithdrawTests is PSMTestBase { _deposit(address(usdc), user1, usdcAmount); _deposit(address(sDai), user2, sDaiAmount); - MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); uint256 user1Shares = usdcAmount * 1e12; uint256 user2Shares = sDaiAmount * 125/100; From 5f1e228f04fb62a91c6d4dcd7bccf603af2597f5 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Wed, 3 Jul 2024 08:39:41 -0400 Subject: [PATCH 9/9] fix: test names --- test/invariant/Invariants.t.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/invariant/Invariants.t.sol b/test/invariant/Invariants.t.sol index e3fb5d7..e336e92 100644 --- a/test/invariant/Invariants.t.sol +++ b/test/invariant/Invariants.t.sol @@ -359,7 +359,7 @@ contract PSMInvariants_TimeBasedRateSetting_NoTransfer is PSMInvariantTestBase { targetContract(address(timeBasedRateHandler)); } - function invariant_A_test() public view { + function invariant_A() public view { _checkInvariant_A(); } @@ -410,7 +410,7 @@ contract PSMInvariants_TimeBasedRateSetting_WithTransfers is PSMInvariantTestBas targetContract(address(transferHandler)); } - function invariant_A_test() public view { + function invariant_A() public view { _checkInvariant_A(); }