diff --git a/test/invariant/Invariants.t.sol b/test/invariant/Invariants.t.sol index 996dccc..b4b5107 100644 --- a/test/invariant/Invariants.t.sol +++ b/test/invariant/Invariants.t.sol @@ -253,6 +253,9 @@ contract PSMInvariants_ConstantRate_NoTransfer is PSMInvariantTestBase { targetContract(address(lpHandler)); targetContract(address(swapperHandler)); + + // Check that LPs used for swap assertions are correct to not get zero values + assertEq(swapperHandler.lp0(), lpHandler.lps(0)); } function invariant_A() public view { @@ -267,7 +270,7 @@ contract PSMInvariants_ConstantRate_NoTransfer is PSMInvariantTestBase { _checkInvariant_C(); } - function invariant_D_test() public view { + function invariant_D() public view { _checkInvariant_D(); } @@ -321,6 +324,9 @@ contract PSMInvariants_RateSetting_NoTransfer is PSMInvariantTestBase { targetContract(address(lpHandler)); targetContract(address(rateSetterHandler)); targetContract(address(swapperHandler)); + + // Check that LPs used for swap assertions are correct to not get zero values + assertEq(swapperHandler.lp0(), lpHandler.lps(0)); } function invariant_A() public view { @@ -331,7 +337,7 @@ contract PSMInvariants_RateSetting_NoTransfer is PSMInvariantTestBase { _checkInvariant_B(); } - function invariant_C_rate() public view { + function invariant_C() public view { _checkInvariant_C(); } @@ -358,9 +364,12 @@ contract PSMInvariants_RateSetting_WithTransfers is PSMInvariantTestBase { targetContract(address(rateSetterHandler)); targetContract(address(swapperHandler)); targetContract(address(transferHandler)); + + // Check that LPs used for swap assertions are correct to not get zero values + assertEq(swapperHandler.lp0(), lpHandler.lps(0)); } - function invariant_A_test() public view { + function invariant_A() public view { _checkInvariant_A(); } @@ -410,6 +419,9 @@ contract PSMInvariants_TimeBasedRateSetting_NoTransfer is PSMInvariantTestBase { targetContract(address(lpHandler)); targetContract(address(swapperHandler)); targetContract(address(timeBasedRateHandler)); + + // Check that LPs used for swap assertions are correct to not get zero values + assertEq(swapperHandler.lp0(), lpHandler.lps(0)); } function invariant_A() public view { @@ -464,9 +476,13 @@ contract PSMInvariants_TimeBasedRateSetting_WithTransfers is PSMInvariantTestBas targetContract(address(swapperHandler)); targetContract(address(timeBasedRateHandler)); targetContract(address(transferHandler)); + + // Check that LPs used for swap assertions are correct to not get zero values + assertEq(swapperHandler.lp0(), lpHandler.lps(0)); } function invariant_A() public view { + _checkInvariant_A(); } diff --git a/test/invariant/handlers/HandlerBase.sol b/test/invariant/handlers/HandlerBase.sol index 86b6536..828236a 100644 --- a/test/invariant/handlers/HandlerBase.sol +++ b/test/invariant/handlers/HandlerBase.sol @@ -67,4 +67,25 @@ contract HandlerBase is CommonBase, StdCheatsSafe, StdUtils { } } + function assertApproxEqRel( + uint256 a, + uint256 b, + uint256 maxPercentDelta, // An 18 decimal fixed point number, where 1e18 == 100% + string memory err + ) internal virtual { + // If the left is 0, right must be too. + if (b == 0) return assertEq(a, b, string(abi.encodePacked("assertEq - ", err))); + + uint256 percentDelta = stdMath.percentDelta(a, b); + + if (percentDelta > maxPercentDelta) { + console.log("Error: a ~= b not satisfied [uint]"); + console.log(" Left", a); + console.log(" Right", b); + console.log(" Max % Delta [wad]", maxPercentDelta); + console.log(" % Delta [wad]", percentDelta); + revert(err); + } + } + } diff --git a/test/invariant/handlers/RateSetterHandler.sol b/test/invariant/handlers/RateSetterHandler.sol index 245b358..d9cbf76 100644 --- a/test/invariant/handlers/RateSetterHandler.sol +++ b/test/invariant/handlers/RateSetterHandler.sol @@ -22,7 +22,7 @@ contract RateSetterHandler is HandlerBase { // 1. Setup and bounds // Increase the rate by up to 20% - rate += bound(rateIncrease, 0, 0.2e27); + rate += _bound(rateIncrease, 0, 0.2e27); // 2. Cache starting state uint256 startingConversion = psm.convertToShares(1e18); diff --git a/test/invariant/handlers/SwapperHandler.sol b/test/invariant/handlers/SwapperHandler.sol index b9fac09..7692a8e 100644 --- a/test/invariant/handlers/SwapperHandler.sol +++ b/test/invariant/handlers/SwapperHandler.sol @@ -11,6 +11,9 @@ contract SwapperHandler is HandlerBase { address[] public swappers; + // Used for assertions, assumption made that LpHandler is used with at least 1 LP. + address public lp0; + uint256 public swapCount; uint256 public zeroBalanceCount; @@ -19,15 +22,18 @@ contract SwapperHandler is HandlerBase { MockERC20 asset0, MockERC20 asset1, MockERC20 asset2, - uint256 lpCount + uint256 swapperCount ) HandlerBase(psm_) { assets[0] = asset0; assets[1] = asset1; assets[2] = asset2; - for (uint256 i = 0; i < lpCount; i++) { + for (uint256 i = 0; i < swapperCount; i++) { swappers.push(makeAddr(string(abi.encodePacked("swapper-", vm.toString(i))))); } + + // Derive LP-0 address for assertion + lp0 = makeAddr("lp-0"); } function _getAsset(uint256 indexSeed) internal view returns (MockERC20) { @@ -85,11 +91,12 @@ contract SwapperHandler is HandlerBase { ); // 2. Cache starting state - uint256 startingConversion = psm.convertToAssetValue(1e18); - uint256 startingValue = psm.getPsmTotalValue(); + uint256 startingConversion = psm.convertToAssetValue(1e18); + uint256 startingConversionMillion = psm.convertToAssetValue(1e6 * 1e18); + uint256 startingConversionLp0 = psm.convertToAssetValue(psm.shares(lp0)); + uint256 startingValue = psm.getPsmTotalValue(); // 3. Perform action against protocol - vm.startPrank(swapper); assetIn.mint(swapper, amountIn); assetIn.approve(address(psm), amountIn); @@ -98,17 +105,29 @@ contract SwapperHandler is HandlerBase { // 4. Perform action-specific assertions - // Performing this check with tighter bounds when there is a minimum amount in the PSM. - // Leads to more accurate assertions in the more realistic scenario. - if (psm.getPsmTotalValue() > 10_000e18) { - // Rounding because of USDC precision - assertApproxEqAbs( - psm.convertToAssetValue(1e18), - startingConversion, - 1e9, - "SwapperHandler/swap/conversion-rate-change" - ); - } + // Rounding because of USDC precision, a user's position can fluctuate by 2e12 per 1e18 shares + assertApproxEqAbs( + psm.convertToAssetValue(1e18), + startingConversion, + 2e12, + "SwapperHandler/swap/conversion-rate-change" + ); + + // Demonstrate rounding scales with shares + assertApproxEqAbs( + psm.convertToAssetValue(1_000_000e18), + startingConversionMillion, + 2_000_000e12, // 2e18 of value + "SwapperHandler/swap/conversion-rate-change" + ); + + // Position values can fluctuate by 0.00000002% on swaps + assertApproxEqRel( + psm.convertToAssetValue(psm.shares(lp0)), + startingConversionLp0, + 0.000002e18, + "SwapperHandler/swap/conversion-rate-change" + ); // Rounding because of USDC precision assertGe(