-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
1 parent
dc40383
commit 427a41b
Showing
20 changed files
with
381 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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/[email protected] | ||
# 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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() | ||
); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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))); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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++; | ||
} | ||
|
||
} |
Oops, something went wrong.