Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add invariant hooks, refactor to handle multiple suites (SC-481) #14

Merged
merged 111 commits into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
111 commits
Select commit Hold shift + click to select a range
822694a
feat: first test working
lucas-manuel May 18, 2024
d6530e1
feat: use larger numbers:
lucas-manuel May 18, 2024
8f11df4
feat: test with initial burn amount passing
lucas-manuel May 18, 2024
78fb4ad
feat: update tests to work with updated burn logic, move conversion f…
lucas-manuel May 21, 2024
d32fd2f
feat: remove todos
lucas-manuel May 21, 2024
2290417
fix: update to remove console and update comment
lucas-manuel May 21, 2024
1fe351b
feat: get swap tests working
lucas-manuel May 22, 2024
cbea0ee
feat: get all swap tests working
lucas-manuel May 22, 2024
7d1101d
fix: update for three assets in logic
lucas-manuel May 22, 2024
80440e0
feat: all tests passing
lucas-manuel May 22, 2024
51b980d
fix: rm commented out test
lucas-manuel May 22, 2024
e6b67c6
feat: add preview swap tests
lucas-manuel May 22, 2024
ffa4513
feat: move logic out of single use internal and use conversion rate e…
lucas-manuel May 22, 2024
e9c519b
feat: move divRoundUp out of single use internal
lucas-manuel May 22, 2024
6ec2b23
feat: add full coverage for conversion tests
lucas-manuel May 22, 2024
2be5afc
feat: add more preview cases
lucas-manuel May 22, 2024
4299e03
feat: refactor PSM to use three assets
lucas-manuel May 22, 2024
64c9939
fix: rm comment
lucas-manuel May 22, 2024
2c43f92
feat: add interface, natspec, events, referral code, tests passing
lucas-manuel May 22, 2024
57c65d6
fix: update to rm consolegp
lucas-manuel May 22, 2024
c0616bf
Merge branch 'sc-447-build-threeway-swap' into sc-448-add-referral-code
lucas-manuel May 22, 2024
416034f
feat: add events testing
lucas-manuel May 23, 2024
114f92b
feat: make precisions internal and add state var natspec
lucas-manuel May 23, 2024
8c2f83b
feat: finish natspec
lucas-manuel May 23, 2024
61443c8
feat: add readme
lucas-manuel May 23, 2024
6ca25a8
feat: add referral code note
lucas-manuel May 23, 2024
c4f8c78
fix: update constructor test
lucas-manuel May 23, 2024
b070a44
fix: update links
lucas-manuel May 23, 2024
6947398
fix: reformatting
lucas-manuel May 23, 2024
9bdcfc5
fix: update testing section
lucas-manuel May 23, 2024
d69bc8b
fix: improve overview
lucas-manuel May 23, 2024
04cc6ac
feat: add emojis
lucas-manuel May 23, 2024
54a4afe
feat: remove all share burn logic, get all non inflation attack tests…
May 30, 2024
ce5653d
fix: cleanup diff
May 30, 2024
259cf16
fix: update to use initial deposit instead of burn
May 30, 2024
62ef5c2
feat: add readme section explaining attack
May 30, 2024
0720150
fix: minimize diff
May 30, 2024
5a03fde
feat: fix conflicts
May 30, 2024
c5f7cf0
feat: rm all references to initial burn, update tests
May 30, 2024
e6c654d
fix: address bartek comments
May 30, 2024
e540afa
feat: update all tests to work with new interfaces
May 30, 2024
c41b010
feat: add deposit failure mode tests
May 30, 2024
847a1db
feat: update to add assertions for return in deposit
May 30, 2024
ecbf99b
feat: add withdraw failure tests
May 30, 2024
44a5d52
feat: update to address comments outside sharesToBurn
Jun 3, 2024
9c3958e
feat: update inflation attack test and readme
Jun 3, 2024
4c9cf09
fix: update readme
Jun 3, 2024
cb9931a
feat: update test to constrain deposit/withdraw
Jun 3, 2024
e576672
feat: update to add both cases
Jun 3, 2024
e994287
fix: merge conflicts
Jun 3, 2024
9688892
fix: merge conflicts
Jun 3, 2024
b80ed6e
fix: merge conflicts
Jun 3, 2024
108624a
fix: merge conflicts
Jun 4, 2024
73d8c8c
feat: update per review
lucas-manuel Jun 4, 2024
d6c6f03
fix: merge conflicts
lucas-manuel Jun 4, 2024
5319255
Merge branch 'sc-448-add-referral-code' into sc-453-add-receiver-depo…
lucas-manuel Jun 4, 2024
0f182b2
feat: update to use underscore bound, fix test
lucas-manuel Jun 5, 2024
dfab7ec
Merge branch 'sc-447-build-threeway-swap' into sc-448-add-referral-code
lucas-manuel Jun 5, 2024
b288568
fix: merge conflicts
lucas-manuel Jun 5, 2024
ba44086
fix: typo
lucas-manuel Jun 5, 2024
62f5423
fix: merge conflicts
lucas-manuel Jun 5, 2024
9a243f9
feat: add overrides, remove referrals, update referral type
lucas-manuel Jun 10, 2024
cc68ed3
fix: update expect emit
lucas-manuel Jun 10, 2024
9862a53
feat: update name and remove todos
lucas-manuel Jun 10, 2024
376fa83
feat: move files and set up structure
lucas-manuel Jun 10, 2024
73cb854
feat: update to rename files, contracts, and errors
lucas-manuel Jun 10, 2024
f711954
fix: merge conflicts
lucas-manuel Jun 10, 2024
23c870d
fix: merge conflicts
lucas-manuel Jun 10, 2024
ce0949f
fix: merge conflicts
lucas-manuel Jun 10, 2024
81d2e37
fix: rm dup file, update toml
lucas-manuel Jun 10, 2024
fbff8d3
feat: get deposits working
lucas-manuel Jun 10, 2024
3e5a3eb
chore: refactor into proper inheritance structure
lucas-manuel Jun 10, 2024
2e60593
feat: get all functions working with reverts
lucas-manuel Jun 10, 2024
14fdf08
fix: cofnlicts
lucas-manuel Jun 13, 2024
defc84e
feat: update conversion
lucas-manuel Jun 13, 2024
bbc3e8e
feat: get swaps working without reverts
lucas-manuel Jun 13, 2024
4c04863
feat: add fully working deposit/withdraw/swaps, invariant_B failing
lucas-manuel Jun 13, 2024
d23bfbe
ci: update for ci
lucas-manuel Jun 13, 2024
7ba3e16
fix: update name
lucas-manuel Jun 13, 2024
499afae
chore: rm basly cased file
lucas-manuel Jun 13, 2024
d1daf4b
chore: re add
lucas-manuel Jun 13, 2024
84c3787
fix: re add invariant
lucas-manuel Jun 13, 2024
3814067
ci: experiment with 2 million total calls
lucas-manuel Jun 13, 2024
9215a29
ci: add show progress flag
lucas-manuel Jun 13, 2024
9caf3c6
fix: move file back
lucas-manuel Jun 13, 2024
094d613
ci: update verbosity
lucas-manuel Jun 13, 2024
2085360
ci: add PR profile
lucas-manuel Jun 13, 2024
0605586
fix: rm redundant files
lucas-manuel Jun 13, 2024
74dba88
feat: update from review changes
lucas-manuel Jun 14, 2024
fa3a232
feat: add afterInvariant hook
lucas-manuel Jun 19, 2024
ba9b5fe
Merge branch 'master' into sc-459-add-basic-invariant-testing
lucas-manuel Jun 20, 2024
608daf5
Merge branch 'master' into sc-459-add-basic-invariant-testing
lucas-manuel Jun 20, 2024
c4bf0cc
fix: update invariant
lucas-manuel Jun 20, 2024
df65c1b
fix: add fuzz failure
lucas-manuel Jun 20, 2024
42581aa
chore: rm indexing comment
lucas-manuel Jun 21, 2024
49fdc1f
Merge branch 'sc-459-add-basic-invariant-testing' into sc-481-add-inv…
lucas-manuel Jun 21, 2024
1b07d93
feat: refactor structure
lucas-manuel Jun 21, 2024
e5153a0
feat: both invariants working
lucas-manuel Jun 21, 2024
c664f2c
fix: update comment
lucas-manuel Jun 21, 2024
54e8ae9
fix: update toml
lucas-manuel Jun 21, 2024
f982ff3
fix: rm redundant files from merge
lucas-manuel Jun 21, 2024
1c3c450
Merge branch 'sc-459-add-basic-invariant-testing' into sc-481-add-inv…
lucas-manuel Jun 21, 2024
ffa6eee
fix: update tolerances
lucas-manuel Jun 21, 2024
a2e6f05
feat: update to add seeding as part of invariants
lucas-manuel Jun 21, 2024
68f41ab
fix: merge branch
lucas-manuel Jun 21, 2024
b705b4c
fix: merge conflicts
lucas-manuel Jun 26, 2024
30473bd
fix: formatting
lucas-manuel Jun 26, 2024
41ea944
Merge branch 'master' into sc-481-add-invariant-hooks-more-suites
lucas-manuel Jun 26, 2024
56045d3
Merge branch 'master' into sc-481-add-invariant-hooks-more-suites
lucas-manuel Jun 26, 2024
de932e1
fix: review changes
lucas-manuel Jun 27, 2024
c44f9f6
fix: rm constant
lucas-manuel Jul 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ runs = 1000
[invariant]
runs = 20
depth = 1000
shrink_run_limit = 0

[profile.pr.invariant]
runs = 200
Expand Down
213 changes: 194 additions & 19 deletions test/invariant/Invariants.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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))) +
Expand All @@ -44,15 +41,15 @@ contract PSMInvariantTests is PSMTestBase {
);
}

function invariant_B() public view {
function _checkInvariant_B() public view {
assertApproxEqAbs(
psm.getPsmTotalValue(),
psm.convertToAssetValue(psm.totalShares()),
2
);
}

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)))) +
Expand All @@ -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());
Expand All @@ -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();
}

}
8 changes: 3 additions & 5 deletions test/invariant/handlers/LpHandler.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ contract LpHandler is HandlerBase {
uint256 public depositCount;
uint256 public withdrawCount;

uint256 public constant TRILLION = 1e12;

constructor(
PSM3 psm_,
MockERC20 asset0,
Expand All @@ -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)))));
}
}

Expand All @@ -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);
Expand All @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion test/invariant/handlers/SwapperHandler.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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)))));
}
}

Expand Down
38 changes: 38 additions & 0 deletions test/invariant/handlers/TransferHandler.sol
Original file line number Diff line number Diff line change
@@ -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;
}

}
Loading