diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 6c7a77887..de29524ec 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -21,7 +21,7 @@ jobs: # Report code coverage to discord - name: Generate coverage - run: forge coverage --report lcov --no-match-test testLoad + run: forge coverage --report lcov --no-match-test "testLoad|invariant" - name: Setup LCOV uses: hrishikesh-kadam/setup-lcov@v1 - name: Filter lcov diff --git a/Makefile b/Makefile index 7143cb4f2..5a0c59780 100644 --- a/Makefile +++ b/Makefile @@ -14,10 +14,11 @@ install :; git submodule update --init --recursive build :; forge clean && forge build # Tests -test :; forge test --no-match-test testLoad # --ffi # enable if you need the `ffi` cheat code on HEVM -test-with-gas-report :; FOUNDRY_PROFILE=optimized forge test --no-match-test testLoad --gas-report # --ffi # enable if you need the `ffi` cheat code on HEVM +test :; forge test --no-match-test "testLoad|invariant" # --ffi # enable if you need the `ffi` cheat code on HEVM +test-with-gas-report :; FOUNDRY_PROFILE=optimized forge test --no-match-test "testLoad|invariant" --gas-report # --ffi # enable if you need the `ffi` cheat code on HEVM test-load :; FOUNDRY_PROFILE=optimized forge test --match-test testLoad --gas-report -coverage :; forge coverage --no-match-test testLoad +test-invariant :; forge t --mt invariant +coverage :; forge coverage --no-match-test "testLoad|invariant" # Generate Gas Snapshots snapshot :; forge clean && forge snapshot diff --git a/check-code-coverage.sh b/check-code-coverage.sh index 72fdb5be2..9bc6ba7ae 100755 --- a/check-code-coverage.sh +++ b/check-code-coverage.sh @@ -1,6 +1,6 @@ #!/bin/bash -forge coverage --report lcov --no-match-test testLoad +forge coverage --report lcov --no-match-test "testLoad|invariant" lcov -r lcov.info "tests/*" -o lcov-filtered.info --rc lcov_branch_coverage=1 diff --git a/foundry.toml b/foundry.toml index 55b2b54cc..c1b3e82d2 100644 --- a/foundry.toml +++ b/foundry.toml @@ -24,3 +24,9 @@ optimizer_runs = 200 [fuzz] runs = 300 + +[invariant] +runs = 10 # The number of calls to make in the invariant tests +depth = 100 # The number of times to run the invariant tests +call_override = false # Override calls +fail_on_revert = false # Fail the test if the contract reverts \ No newline at end of file diff --git a/src/base/Pool.sol b/src/base/Pool.sol index b39d0ff16..36a7a7e84 100644 --- a/src/base/Pool.sol +++ b/src/base/Pool.sol @@ -715,4 +715,19 @@ abstract contract Pool is Clone, ReentrancyGuard, Multicall, IPool { reserveAuction.kicked ); } + + /// @inheritdoc IPoolState + function totalAuctionsInPool() external view override returns (uint256) { + return auctions.noOfAuctions; + } + + /// @inheritdoc IPoolState + function totalT0Debt() external view override returns (uint256) { + return poolBalances.t0Debt; + } + + /// @inheritdoc IPoolState + function totalT0DebtInAuction() external view override returns (uint256) { + return poolBalances.t0DebtInAuction; + } } diff --git a/src/interfaces/pool/commons/IPoolState.sol b/src/interfaces/pool/commons/IPoolState.sol index c5e5593b6..bfda7af77 100644 --- a/src/interfaces/pool/commons/IPoolState.sol +++ b/src/interfaces/pool/commons/IPoolState.sol @@ -222,6 +222,26 @@ interface IPoolState { */ function pledgedCollateral() external view returns (uint256); + /** + * @notice Returns the total number of active auctions in pool + * @return totalAuctions_ number of active auctions. + */ + function totalAuctionsInPool() external view returns (uint256); + + /** + * @notice Returns the `t0Debt` state variable. + * @dev This value should be multiplied by inflator in order to calculate current debt of the pool. + * @return The total t0Debt in the system, in WAD units. + */ + function totalT0Debt() external view returns (uint256); + + /** + * @notice Returns the `t0DebtInAuction` state variable. + * @dev This value should be multiplied by inflator in order to calculate current debt in auction of the pool. + * @return The total t0DebtInAuction in the system, in WAD units. + */ + function totalT0DebtInAuction() external view returns (uint256); + } /*********************/ diff --git a/tests/INVARIANTS.md b/tests/INVARIANTS.md new file mode 100644 index 000000000..8799e40a3 --- /dev/null +++ b/tests/INVARIANTS.md @@ -0,0 +1,69 @@ +# Ajna Pool Invariants + +## Collateral +- #### ERC20: + - **CT1**: pool collateral token balance (`Collateral.balanceOf(pool)`) = sum of collateral balances across all borrowers (`Borrower.collateral`) + sum of claimable collateral across all buckets (`Bucket.collateral`) +- #### NFT: + - **CT2**: number of tokens owned by the pool (`Collateral.balanceOf(pool)`) * `1e18` = sum of collateral across all borrowers (`Borrower.collateral`) + sum of claimable collateral across all buckets (`Bucket.collateral`) + - **CT3**: number of tokens owned by the pool (`Collateral.balanceOf(pool)` = length of borrower array token ids (`ERC721Pool.borrowerTokenIds.length`) + length of buckets array token ids (`ERC721Pool.bucketTokenIds.length`) + - **CT4**: number of borrower token ids (`ERC721Pool.borrowerTokenIds.length`) * `1e18` <= borrower balance (`Borrower.collateral`) Note: can be lower in case when fractional collateral that is rebalanced / moved to buckets claimable token ids + - **CT5**: token ids in buckets array (`ERC721Pool.bucketTokenIds`) and in borrowers array (`ERC721Pool.borrowerTokenIds`) are owned by pool contract (`Collateral.ownerOf(tokenId)`) + - **CT6**: in case of subset pools: token ids in buckets array (`ERC721Pool.bucketTokenIds`) and in borrowers array (`ERC721Pool.borrowerTokenIds`) should have a mapping of `True` in allowed token ids mapping (`ERC721Pool.tokenIdsAllowed`) + +- **CT7**: total pledged collateral in pool (`PoolBalancesState.pledgedCollateral`) = sum of collateral balances across all borrowers (`Borrower.collateral`) + +## Quote Token +- **QT1**: pool quote token balance (`Quote.balanceOf(pool)`) >= liquidation bonds (`AuctionsState.totalBondEscrowed`) + pool deposit size (`Pool.depositSize()`) + reserve auction unclaimed amount (`reserveAuction.unclaimed`) - pool t0 debt (`PoolBalancesState.t0Debt`) +- **QT2**: pool t0 debt (`PoolBalancesState.t0Debt`) = sum of t0 debt across all borrowers (`Borrower.t0Debt`) + +## Auctions +- **A1**: total t0 debt auctioned (`PoolBalancesState.t0DebtInAuction`) = sum of debt across all auctioned borrowers (`Borrower.t0Debt` where borrower's `kickTime != 0`) +- **A2**: sum of bonds locked in auctions (`Liquidation.bondSize`) = sum of locked balances across all kickers (`Kicker.locked`) = total bond escrowed accumulator (`AuctionsState.totalBondEscrowed`) +- **A3**: number of borrowers with debt (`LoansState.borrowers.length` with `t0Debt != 0`) = number of loans (`LoansState.loans.length -1`) + number of auctioned borrowers (`AuctionsState.noOfAuctions`) +- **A4**: number of recorded auctions (`AuctionsState.noOfAuctions`) = length of auctioned borrowers (count of borrowers in `AuctionsState.liquidations` with `kickTime != 0`) +- **A5**: for each `Liquidation` recorded in liquidation mapping (`AuctionsState.liquidations`) the kicker address (`Liquidation.kicker`) has a locked balance (`Kicker.locked`) equal or greater than liquidation bond size (`Liquidation.bondSize`) +- **A6**: if a `Liquidation` is not taken then the take flag (`Liquidation.alreadyTaken`) should be `False`, if already taken then the take flag should be `True` + +## Loans +- **L1**: for each `Loan` in loans array (`LoansState.loans`) starting from index 1, the corresponding address (`Loan.borrower`) is not `0x`, the threshold price (`Loan.thresholdPrice`) is different than 0 and the id mapped in indices mapping (`LoansState.indices`) equals index of loan in loans array. +- **L2**: `Loan` in loans array (`LoansState.loans`) at index 0 has the corresponding address (`Loan.borrower`) equal with `0x` address and the threshold price (`Loan.thresholdPrice`) equal with 0 +- **L3**: Loans array (`LoansState.loans`) is a max-heap with respect to t0-threshold price: the t0TP of loan at index `i` is >= the t0-threshold price of the loans at index `2*i` and `2*i+1` + +## Buckets +- **B1**: sum of LPs of lenders in bucket (`Lender.lps`) = bucket LPs accumulator (`Bucket.lps`) +- **B2**: bucket LPs accumulator (`Bucket.lps`) = 0 if no deposit / collateral in bucket +- **B3**: if no collateral or deposit in bucket then the bucket exchange rate is `1e27` +- **B4**: bankrupt bucket LPs accumulator = 0; lender LPs for deposits before bankruptcy time = 0 +- **B5**: when adding quote tokens: lender deposit time (`Lender.depositTime`) = timestamp of block when deposit happened (`block.timestamp`) + +## Interest +- **I1**: interest rate (`InterestState.interestRate`) cannot be updated more than once in a 12 hours period of time (`InterestState.interestRateUpdate`) +- **I2**: reserve interest (`ReserveAuctionState.totalInterestEarned`) accrues only once per block (`block.timestamp - InflatorState.inflatorUpdate != 0`) and only if there's debt in the pool (`PoolBalancesState.t0Debt != 0`) +- **I3**: pool inflator (`InflatorState.inflator`) cannot be updated more than once per block (`block.timestamp - InflatorState.inflatorUpdate != 0`) and equals `1e18` if there's no debt in the pool (`PoolBalancesState.t0Debt != 0`) + +## Fenwick tree +- **F1**: Value represented at index `i` (`Deposits.valueAt(i)`) is equal to the accumulation of scaled values incremented or decremented from index `i` +- **F2**: For any index `i`, the prefix sum up to and including `i` is the sum of values stored in indices `j<=i` +- **F3**: For any index `i < MAX_FENWICK_INDEX`, `findIndexOfSum(prefixSum(i)) > i` +- **F4**: For any index `i`, there is zero deposit above `i` and below `findIndexOfSum(prefixSum(i))`: `prefixSum(findIndexOfSum(prefixSum(i))-1 == prefixSum(i)' + +## Exchange rate invariants ## +- **R1**: Exchange rates are unchanged by pledging collateral +- **R2**: Exchange rates are unchanged by removing collateral +- **R3**: Exchange rates are unchanged by depositing quote token into a bucket +- **R4**: Exchange rates are unchanged by withdrawing deposit (quote token) from a bucket +- **R5**: Exchange rates are unchanged by adding collateral token into a bucket +- **R6**: Exchange rates are unchanged by removing collateral token from a bucket +- **R7**: Exchange rates are unchanged under depositTakes +- **R8**: Exchange rates are unchanged under arbTakes + +## Reserves ## +- **RE1**: Reserves are unchanged by pledging collateral +- **RE2**: Reserves are unchanged by removing collateral +- **RE3**: Reserves are unchanged by depositing quote token into a bucket +- **RE4**: Reserves are unchanged by withdrawing deposit (quote token) from a bucket after the penalty period hes expired +- **RE5**: Reserves are unchanged by adding collateral token into a bucket +- **RE6**: Reserves are unchanged by removing collateral token from a bucket +- **RE7**: Reserves increase by 7% of the loan quantity upon the first take (including depositTake or arbTake) +- **RE8**: Reserves are unchanged under takes/depositTakes/arbTakes after the first take +- **RE9**: Reserves increase by .25% of the debt when a loan is kicked diff --git a/tests/README.md b/tests/README.md index 2fc707430..7cccef612 100644 --- a/tests/README.md +++ b/tests/README.md @@ -12,6 +12,10 @@ make test-with-gas-report ```bash make test-load ``` +- run invariant tests: +```bash +make test-invariant +``` - generate code coverage report: ```bash make coverage diff --git a/tests/forge/ERC20Pool/invariants/BasicInvariants.t.sol b/tests/forge/ERC20Pool/invariants/BasicInvariants.t.sol new file mode 100644 index 000000000..55062cabf --- /dev/null +++ b/tests/forge/ERC20Pool/invariants/BasicInvariants.t.sol @@ -0,0 +1,248 @@ + +// SPDX-License-Identifier: UNLICENSED + +pragma solidity 0.8.14; + +import '@std/Test.sol'; +import "@std/console.sol"; + +import { TestBase } from './TestBase.sol'; + +import { Maths } from 'src/libraries/internal/Maths.sol'; + +import { LENDER_MIN_BUCKET_INDEX, LENDER_MAX_BUCKET_INDEX, BORROWER_MIN_BUCKET_INDEX, BasicPoolHandler } from './handlers/BasicPoolHandler.sol'; + +import { IBaseHandler } from './handlers/IBaseHandler.sol'; + +// contains invariants for the test +contract BasicInvariants is TestBase { + + /**************************************************************************************************************************************/ + /*** Invariant Tests ***/ + /*************************************************************************************************************************************** + * Bucket + * B1: totalBucketLPs === totalLenderLps + * B2: bucketLps == 0 (if bucket quote and collateral is 0) + * B3: exchangeRate == 0 (if bucket quote and collateral is 0) + * Quote Token + * QT1: poolQtBal + poolDebt >= totalBondEscrowed + poolDepositSize + * QT2: pool t0 debt = sum of all borrower's t0 debt + + * Collateral Token + * CT1: poolCtBal >= sum of all borrower's collateral + sum of all bucket's claimable collateral + * CT7: pool Pledged collateral = sum of all borrower's pledged collateral + + * Loan + * L1: for each Loan in loans array (LoansState.loans) starting from index 1, the corresponding address (Loan.borrower) is not 0x, the threshold price (Loan.thresholdPrice) is different than 0 + * L2: Loan in loans array (LoansState.loans) at index 0 has the corresponding address (Loan.borrower) equal with 0x address and the threshold price (Loan.thresholdPrice) equal with 0 + * L3: Loans array (LoansState.loans) is a max-heap with respect to t0-threshold price: the t0TP of loan at index i is >= the t0-threshold price of the loans at index 2i and 2i+1 + + * Interest Rate + * I1: Interest rate should only update once in 12 hours + * I3: Inflator should only update once per block + ****************************************************************************************************************************************/ + + uint256 internal constant NUM_ACTORS = 10; + BasicPoolHandler internal _basicPoolHandler; + address internal _handler; + + // bucket exchange rate tracking + mapping(uint256 => uint256) internal previousBucketExchangeRate; + + uint256 previousInterestRateUpdate; + + uint256 previousInflator; + + uint256 previousInflatorUpdate; + + function setUp() public override virtual{ + + super.setUp(); + + _basicPoolHandler = new BasicPoolHandler(address(_pool), address(_quote), address(_collateral), address(_poolInfo), NUM_ACTORS); + _handler = address(_basicPoolHandler); + excludeContract(address(_collateral)); + excludeContract(address(_quote)); + excludeContract(address(_poolFactory)); + excludeContract(address(_pool)); + excludeContract(address(_poolInfo)); + excludeContract(address(_impl)); + + for (uint256 bucketIndex = LENDER_MIN_BUCKET_INDEX; bucketIndex <= LENDER_MAX_BUCKET_INDEX; bucketIndex++) { + ( , , , , ,uint256 exchangeRate) = _poolInfo.bucketInfo(address(_pool), bucketIndex); + previousBucketExchangeRate[bucketIndex] = exchangeRate; + } + + (, previousInterestRateUpdate) = _pool.interestRateInfo(); + + // TODO: Change once this issue is resolved -> https://github.com/foundry-rs/foundry/issues/2963 + targetSender(address(0x1234)); + } + + // checks pool lps are equal to sum of all lender lps in a bucket + function invariant_Lps_B1() public { + uint256 actorCount = IBaseHandler(_handler).getActorsCount(); + for (uint256 bucketIndex = LENDER_MIN_BUCKET_INDEX; bucketIndex <= LENDER_MAX_BUCKET_INDEX; bucketIndex++) { + uint256 totalLps; + for (uint256 i = 0; i < actorCount; i++) { + address lender = IBaseHandler(_handler).actors(i); + (uint256 lps, ) = _pool.lenderInfo(bucketIndex, lender); + totalLps += lps; + } + (uint256 bucketLps, , , , ) = _pool.bucketInfo(bucketIndex); + assertEq(bucketLps, totalLps, "Incorrect Bucket/lender lps"); + } + } + + // checks bucket lps are equal to 0 if bucket quote and collateral are 0 + // checks exchange rate is 1e27 if bucket quote and collateral are 0 + function invariant_Buckets_B2_B3() public view { + for (uint256 bucketIndex = LENDER_MIN_BUCKET_INDEX; bucketIndex <= LENDER_MAX_BUCKET_INDEX; bucketIndex++) { + ( ,uint256 deposit, uint256 collateral, uint256 bucketLps, ,uint256 exchangeRate) = _poolInfo.bucketInfo(address(_pool), bucketIndex); + + if (collateral == 0 && deposit == 0) { + require(bucketLps == 0, "Incorrect bucket lps"); + require(exchangeRate == 1e18, "Incorrect exchange rate"); + } + } + } + + // checks pool quote token balance is greater than equals total deposits in pool + function invariant_quoteTokenBalance_QT1() public { + uint256 poolBalance = _quote.balanceOf(address(_pool)); + uint256 t0debt = _pool.totalT0Debt(); + (uint256 inflator, ) = _pool.inflatorInfo(); + uint256 poolDebt = Maths.wmul(t0debt, inflator); + (uint256 totalPoolBond, uint256 unClaimed, ) = _pool.reservesInfo(); + + assertGe(poolBalance + poolDebt, totalPoolBond + _pool.depositSize() + unClaimed, "Incorrect pool debt"); + } + + // checks pools collateral Balance to be equal to collateral pledged + function invariant_collateralBalance_CT1_CT7() public { + uint256 actorCount = IBaseHandler(_handler).getActorsCount(); + uint256 totalCollateralPledged; + for(uint256 i = 0; i < actorCount; i++) { + address borrower = IBaseHandler(_handler).actors(i); + ( , uint256 borrowerCollateral, ) = _pool.borrowerInfo(borrower); + totalCollateralPledged += borrowerCollateral; + } + + assertEq(_pool.pledgedCollateral(), totalCollateralPledged, "Incorrect Collateral Pledged"); + + uint256 collateralBalance = _collateral.balanceOf(address(_pool)); + uint256 bucketCollateral; + + for (uint256 bucketIndex = LENDER_MIN_BUCKET_INDEX; bucketIndex <= LENDER_MAX_BUCKET_INDEX; bucketIndex++) { + (, , uint256 collateral , , ) = _pool.bucketInfo(bucketIndex); + bucketCollateral += collateral; + } + + assertGe(collateralBalance, bucketCollateral + _pool.pledgedCollateral()); + } + + // checks pool debt is equal to sum of all borrowers debt + function invariant_pooldebt_QT2() public view { + uint256 actorCount = IBaseHandler(_handler).getActorsCount(); + uint256 totalDebt; + for(uint256 i = 0; i < actorCount; i++) { + address borrower = IBaseHandler(_handler).actors(i); + (uint256 debt, , ) = _pool.borrowerInfo(borrower); + totalDebt += debt; + } + + uint256 poolDebt = _pool.totalT0Debt(); + + require(poolDebt == totalDebt, "Incorrect pool debt"); + } + + function _invariant_exchangeRate_RE1_RE2_R3_R4_R5_R6() public { + for (uint256 bucketIndex = LENDER_MIN_BUCKET_INDEX; bucketIndex <= LENDER_MAX_BUCKET_INDEX; bucketIndex++) { + ( , , , , ,uint256 exchangeRate) = _poolInfo.bucketInfo(address(_pool), bucketIndex); + if (!IBaseHandler(_handler).shouldExchangeRateChange()) { + console.log("======================================"); + console.log("Bucket Index -->", bucketIndex); + console.log("Previous exchange Rate -->", previousBucketExchangeRate[bucketIndex]); + console.log("Current exchange Rate -->", exchangeRate); + requireWithinDiff(exchangeRate, previousBucketExchangeRate[bucketIndex], 1e12, "Incorrect exchange Rate changed"); + console.log("======================================"); + } + previousBucketExchangeRate[bucketIndex] = exchangeRate; + } + } + + function invariant_loan_L1_L2_L3() public view { + (address borrower, uint256 tp) = _pool.loanInfo(0); + + // first loan in loan heap should be 0 + require(borrower == address(0), "Incorrect borrower"); + require(tp == 0, "Incorrect threshold price"); + + ( , , uint256 totalLoans) = _pool.loansInfo(); + + for(uint256 loanId = 1; loanId < totalLoans; loanId++) { + (borrower, tp) = _pool.loanInfo(loanId); + + // borrower address and threshold price should not 0 + require(borrower != address(0), "Incorrect borrower"); + require(tp != 0, "Incorrect threshold price"); + + // tp of a loan at index 'i' in loan array should be greater than equals to loans at index '2i' and '2i+1' + (, uint256 tp1) = _pool.loanInfo(2 * loanId); + (, uint256 tp2) = _pool.loanInfo(2 * loanId + 1); + + require(tp >= tp1, "Incorrect loan heap"); + require(tp >= tp2, "Incorrect loan heap"); + } + } + + // interest should only update once in 12 hours + function invariant_interest_rate_I1() public { + + (, uint256 currentInterestRateUpdate) = _pool.interestRateInfo(); + + if (currentInterestRateUpdate != previousInterestRateUpdate) { + require(currentInterestRateUpdate - previousInterestRateUpdate >= 12 hours, "Incorrect interest rate update"); + } + previousInterestRateUpdate = currentInterestRateUpdate; + } + + // inflator should only update once per block + function invariant_inflator_I3() public { + (uint256 currentInflator, uint256 currentInflatorUpdate) = _pool.inflatorInfo(); + if(currentInflatorUpdate == previousInflatorUpdate) { + require(currentInflator == previousInflator, "Incorrect inflator update"); + } + previousInflator = currentInflator; + previousInflatorUpdate = currentInflatorUpdate; + } + + function invariant_call_summary() external view virtual { + console.log("\nCall Summary\n"); + console.log("--Lender----------"); + console.log("BBasicHandler.addQuoteToken ", IBaseHandler(_handler).numberOfCalls("BBasicHandler.addQuoteToken")); + console.log("UBBasicHandler.addQuoteToken ", IBaseHandler(_handler).numberOfCalls("UBBasicHandler.addQuoteToken")); + console.log("BBasicHandler.removeQuoteToken ", IBaseHandler(_handler).numberOfCalls("BBasicHandler.removeQuoteToken")); + console.log("UBBasicHandler.removeQuoteToken ", IBaseHandler(_handler).numberOfCalls("UBBasicHandler.removeQuoteToken")); + console.log("BBasicHandler.addCollateral ", IBaseHandler(_handler).numberOfCalls("BBasicHandler.addCollateral")); + console.log("UBBasicHandler.addCollateral ", IBaseHandler(_handler).numberOfCalls("UBBasicHandler.addCollateral")); + console.log("BBasicHandler.removeCollateral ", IBaseHandler(_handler).numberOfCalls("BBasicHandler.removeCollateral")); + console.log("UBBasicHandler.removeCollateral ", IBaseHandler(_handler).numberOfCalls("UBBasicHandler.removeCollateral")); + console.log("--Borrower--------"); + console.log("BBasicHandler.drawDebt ", IBaseHandler(_handler).numberOfCalls("BBasicHandler.drawDebt")); + console.log("UBBasicHandler.drawDebt ", IBaseHandler(_handler).numberOfCalls("UBBasicHandler.drawDebt")); + console.log("BBasicHandler.repayDebt ", IBaseHandler(_handler).numberOfCalls("BBasicHandler.repayDebt")); + console.log("UBBasicHandler.repayDebt ", IBaseHandler(_handler).numberOfCalls("UBBasicHandler.repayDebt")); + console.log("------------------"); + console.log( + "Sum", + IBaseHandler(_handler).numberOfCalls("BBasicHandler.addQuoteToken") + + IBaseHandler(_handler).numberOfCalls("BBasicHandler.removeQuoteToken") + + IBaseHandler(_handler).numberOfCalls("BBasicHandler.addCollateral") + + IBaseHandler(_handler).numberOfCalls("BBasicHandler.removeCollateral") + + IBaseHandler(_handler).numberOfCalls("BBasicHandler.drawDebt") + + IBaseHandler(_handler).numberOfCalls("BBasicHandler.repayDebt") + ); + } + +} \ No newline at end of file diff --git a/tests/forge/ERC20Pool/invariants/InvariantTest.sol b/tests/forge/ERC20Pool/invariants/InvariantTest.sol new file mode 100644 index 000000000..a1e7ab0e6 --- /dev/null +++ b/tests/forge/ERC20Pool/invariants/InvariantTest.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity 0.8.14; + +contract InvariantTest { + + struct FuzzSelector { + address addr; + bytes4[] selectors; + } + + address[] private _excludedContracts; + address[] private _excludedSenders; + address[] private _targetedContracts; + address[] private _targetedSenders; + + FuzzSelector[] internal _targetedSelectors; + + function excludeContract(address newExcludedContract_) internal { + _excludedContracts.push(newExcludedContract_); + } + + function excludeContracts() public view returns (address[] memory excludedContracts_) { + require(_excludedContracts.length != uint256(0), "NO_EXCLUDED_CONTRACTS"); + excludedContracts_ = _excludedContracts; + } + + function excludeSender(address newExcludedSender_) internal { + _excludedSenders.push(newExcludedSender_); + } + + function excludeSenders() public view returns (address[] memory excludedSenders_) { + require(_excludedSenders.length != uint256(0), "NO_EXCLUDED_SENDERS"); + excludedSenders_ = _excludedSenders; + } + + function targetContract(address newTargetedContract_) internal { + _targetedContracts.push(newTargetedContract_); + } + + function targetContracts() public view returns (address[] memory targetedContracts_) { + require(_targetedContracts.length != uint256(0), "NO_TARGETED_CONTRACTS"); + targetedContracts_ = _targetedContracts; + } + + function targetSelector(FuzzSelector memory newTargetedSelector_) internal { + _targetedSelectors.push(newTargetedSelector_); + } + + function targetSelectors() public view returns (FuzzSelector[] memory targetedSelectors_) { + require(targetedSelectors_.length != uint256(0), "NO_TARGETED_SELECTORS"); + targetedSelectors_ = _targetedSelectors; + } + + function targetSender(address newTargetedSender_) internal { + _targetedSenders.push(newTargetedSender_); + } + + function targetSenders() public view returns (address[] memory targetedSenders_) { + require(_targetedSenders.length != uint256(0), "NO_TARGETED_SENDERS"); + targetedSenders_ = _targetedSenders; + } + + /************************/ + /*** Helper Functions ***/ + /************************/ + + function getDiff(uint256 x, uint256 y) internal pure returns (uint256 diff) { + diff = x > y ? x - y : y - x; + } + + function requireWithinDiff(uint256 x, uint256 y, uint256 expectedDiff, string memory err) internal pure { + require(getDiff(x, y) <= expectedDiff, err); + } + +} \ No newline at end of file diff --git a/tests/forge/ERC20Pool/invariants/LiquidationInvariant.t.sol b/tests/forge/ERC20Pool/invariants/LiquidationInvariant.t.sol new file mode 100644 index 000000000..b455e9d8b --- /dev/null +++ b/tests/forge/ERC20Pool/invariants/LiquidationInvariant.t.sol @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity 0.8.14; + +import '@std/Test.sol'; +import "@std/console.sol"; + +import { TestBase } from './TestBase.sol'; + +import { LENDER_MIN_BUCKET_INDEX, LENDER_MAX_BUCKET_INDEX, BORROWER_MIN_BUCKET_INDEX } from './handlers/BasicPoolHandler.sol'; + +import { LiquidationPoolHandler } from './handlers/LiquidationPoolHandler.sol'; +import { BasicInvariants } from './BasicInvariants.t.sol'; +import { IBaseHandler } from './handlers/IBaseHandler.sol'; + +contract LiquidationInvariant is BasicInvariants { + + /**************************************************************************************************************************************/ + /*** Invariant Tests ***/ + /*************************************************************************************************************************************** + * Auction + * A1: totalDebtInAuction = sum of all debt of all borrowers kicked + * A2: totalBondEscrowed = sum of all kicker's bond = total Bond in Auction + * A3: number of borrowers with debt = number of loans + number of auctioned borrowers + * A4: number of auctions = total borrowers kicked + * A5: for each auction, kicker locked bond is more than equal to auction bond + ****************************************************************************************************************************************/ + + LiquidationPoolHandler internal _liquidationPoolHandler; + + function setUp() public override virtual{ + + super.setUp(); + + excludeContract(address(_basicPoolHandler)); + + _liquidationPoolHandler = new LiquidationPoolHandler(address(_pool), address(_quote), address(_collateral), address(_poolInfo), NUM_ACTORS); + _handler = address(_liquidationPoolHandler); + } + + // checks sum of all borrower's t0debt is equals to total pool t0debtInAuction + function invariant_debtInAuction_A1() public view { + uint256 actorCount = IBaseHandler(_handler).getActorsCount(); + uint256 totalT0debtInAuction; + for(uint256 i = 0; i < actorCount; i++) { + address borrower = IBaseHandler(_handler).actors(i); + (, , , uint256 kickTime, , , , , ) = _pool.auctionInfo(borrower); + if(kickTime != 0) { + (uint256 t0debt, , ) = _pool.borrowerInfo(borrower); + totalT0debtInAuction += t0debt; + } + } + require(_pool.totalT0DebtInAuction() == totalT0debtInAuction, "Incorrect debt in auction"); + } + + // checks sum of all kicker bond is equal to total pool bond + function invariant_bond_A2() public view { + uint256 actorCount = IBaseHandler(_handler).getActorsCount(); + uint256 totalKickerBond; + for(uint256 i = 0; i < actorCount; i++) { + address kicker = IBaseHandler(_handler).actors(i); + (, uint256 bond) = _pool.kickerInfo(kicker); + totalKickerBond += bond; + } + + uint256 totalBondInAuction; + + for(uint256 i = 0; i < actorCount; i++) { + address borrower = IBaseHandler(_handler).actors(i); + (, , uint256 bondSize, , , , , , ) = _pool.auctionInfo(borrower); + totalBondInAuction += bondSize; + } + + require(totalBondInAuction == totalKickerBond, "Incorrect bond"); + + (uint256 totalPoolBond, , ) = _pool.reservesInfo(); + + require(totalPoolBond == totalKickerBond, "Incorrect bond"); + } + + // checks total borrowers with debt is equals to sum of borrowers unkicked and borrowers kicked + // checks total auctions is equals to total borrowers kicked + function invariant_auctions_A3_A4() public view { + uint256 actorCount = IBaseHandler(_handler).getActorsCount(); + uint256 totalBorrowersWithDebt; + for(uint256 i = 0; i < actorCount; i++) { + address borrower = IBaseHandler(_handler).actors(i); + (uint256 t0Debt, , ) = _pool.borrowerInfo(borrower); + if(t0Debt > 0) { + totalBorrowersWithDebt += 1; + } + } + ( , , uint256 loansCount) = _pool.loansInfo(); + uint256 totalAuction = _pool.totalAuctionsInPool(); + require(totalBorrowersWithDebt == loansCount + totalAuction, "incorrect no of borrowers in LoanState"); + + uint256 borrowersKicked; + for(uint256 i = 0; i < actorCount; i++) { + address borrower = IBaseHandler(_handler).actors(i); + (, , , uint256 kickTime, , , , , ) = _pool.auctionInfo(borrower); + if(kickTime != 0) { + borrowersKicked += 1; + } + } + require(borrowersKicked == totalAuction, "Incorrect borrowers in auction"); + } + + function invariant_borrowers_A5() public view { + uint256 actorCount = IBaseHandler(_handler).getActorsCount(); + for(uint256 i = 0; i < actorCount; i++) { + address borrower = IBaseHandler(_handler).actors(i); + (address kicker, , uint256 bondSize, , , , , , ) = _pool.auctionInfo(borrower); + (, uint256 lockedAmount) = _pool.kickerInfo(kicker); + require(lockedAmount >= bondSize, "Incorrect bond locked"); + } + } + + function invariant_call_summary() external view virtual override{ + console.log("\nCall Summary\n"); + console.log("--Lender----------"); + console.log("BLiquidationHandler.addQuoteToken ", IBaseHandler(_handler).numberOfCalls("BBasicHandler.addQuoteToken")); + console.log("UBLiquidationHandler.addQuoteToken ", IBaseHandler(_handler).numberOfCalls("UBBasicHandler.addQuoteToken")); + console.log("BLiquidationHandler.removeQuoteToken ", IBaseHandler(_handler).numberOfCalls("BBasicHandler.removeQuoteToken")); + console.log("UBLiquidationHandler.removeQuoteToken ", IBaseHandler(_handler).numberOfCalls("UBBasicHandler.removeQuoteToken")); + console.log("BLiquidationHandler.addCollateral ", IBaseHandler(_handler).numberOfCalls("BBasicHandler.addCollateral")); + console.log("UBLiquidationHandler.addCollateral ", IBaseHandler(_handler).numberOfCalls("UBBasicHandler.addCollateral")); + console.log("BLiquidationHandler.removeCollateral ", IBaseHandler(_handler).numberOfCalls("BBasicHandler.removeCollateral")); + console.log("UBLiquidationHandler.removeCollateral ", IBaseHandler(_handler).numberOfCalls("UBBasicHandler.removeCollateral")); + console.log("--Borrower--------"); + console.log("BLiquidationHandler.drawDebt ", IBaseHandler(_handler).numberOfCalls("BBasicHandler.drawDebt")); + console.log("UBLiquidationHandler.drawDebt ", IBaseHandler(_handler).numberOfCalls("UBBasicHandler.drawDebt")); + console.log("BLiquidationHandler.repayDebt ", IBaseHandler(_handler).numberOfCalls("BBasicHandler.repayDebt")); + console.log("UBLiquidationHandler.repayDebt ", IBaseHandler(_handler).numberOfCalls("UBBasicHandler.repayDebt")); + console.log("BLiquidationHandler.kickAuction ", IBaseHandler(_handler).numberOfCalls("BLiquidationHandler.kickAuction")); + console.log("UBLiquidationHandler.kickAuction ", IBaseHandler(_handler).numberOfCalls("UBLiquidationHandler.kickAuction")); + console.log("BLiquidationHandler.takeAuction ", IBaseHandler(_handler).numberOfCalls("BLiquidationHandler.takeAuction")); + console.log("UBLiquidationHandler.takeAuction ", IBaseHandler(_handler).numberOfCalls("UBLiquidationHandler.takeAuction")); + console.log("------------------"); + console.log( + "Sum", + IBaseHandler(_handler).numberOfCalls("BBasicHandler.addQuoteToken") + + IBaseHandler(_handler).numberOfCalls("BBasicHandler.removeQuoteToken") + + IBaseHandler(_handler).numberOfCalls("BBasicHandler.addCollateral") + + IBaseHandler(_handler).numberOfCalls("BBasicHandler.removeCollateral") + + IBaseHandler(_handler).numberOfCalls("BBasicHandler.drawDebt") + + IBaseHandler(_handler).numberOfCalls("BBasicHandler.repayDebt") + + IBaseHandler(_handler).numberOfCalls("BLiquidationHandler.kickAuction") + + IBaseHandler(_handler).numberOfCalls("BLiquidationHandler.takeAuction") + ); + } + +} \ No newline at end of file diff --git a/tests/forge/ERC20Pool/invariants/ReserveInvariants.t.sol b/tests/forge/ERC20Pool/invariants/ReserveInvariants.t.sol new file mode 100644 index 000000000..0b9095fc8 --- /dev/null +++ b/tests/forge/ERC20Pool/invariants/ReserveInvariants.t.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity 0.8.14; + +import '@std/Test.sol'; +import "@std/console.sol"; + +import { TestBase } from './TestBase.sol'; + +import { LENDER_MIN_BUCKET_INDEX, LENDER_MAX_BUCKET_INDEX, BORROWER_MIN_BUCKET_INDEX } from './handlers/BasicPoolHandler.sol'; + +import { ReservePoolHandler } from './handlers/ReservePoolHandler.sol'; +import { LiquidationInvariant } from './LiquidationInvariant.t.sol'; +import { IBaseHandler } from './handlers/IBaseHandler.sol'; + +contract ReserveInvariants is LiquidationInvariant { + + ReservePoolHandler internal _reservePoolHandler; + uint256 previousReserves; + + function setUp() public override virtual { + + super.setUp(); + + excludeContract(address(_liquidationPoolHandler)); + + _reservePoolHandler = new ReservePoolHandler(address(_pool), address(_quote), address(_collateral), address(_poolInfo), NUM_ACTORS); + _handler = address(_reservePoolHandler); + + (previousReserves, , , , ) = _poolInfo.poolReservesInfo(address(_pool)); + } + + // FIXME + function _invariant_reserves_RE1_RE2_RE3_RE4_RE5_RE6_RE7_RE8_RE9() public { + + (uint256 currentReserves, , , , ) = _poolInfo.poolReservesInfo(address(_pool)); + console.log("Current Reserves -->", currentReserves); + console.log("Previous Reserves -->", previousReserves); + if(!IBaseHandler(_handler).shouldReserveChange()) { + require(currentReserves == previousReserves, "Incorrect Reserves change"); + } + + uint256 firstTakeIncreaseInReserve = IBaseHandler(_handler).firstTakeIncreaseInReserve(); + + console.log("firstTakeIncreaseInReserve -->", firstTakeIncreaseInReserve); + if(IBaseHandler(_handler).firstTake()) { + requireWithinDiff(currentReserves, previousReserves + firstTakeIncreaseInReserve, 1e2, "Incorrect Reserves change with first take"); + } + + uint256 loanKickIncreaseInReserve = IBaseHandler(_handler).loanKickIncreaseInReserve(); + + console.log("loanKickIncreaseInReserve -->", loanKickIncreaseInReserve); + if(loanKickIncreaseInReserve != 0) { + requireWithinDiff(currentReserves, previousReserves + loanKickIncreaseInReserve, 1e2, "Incorrect Reserves change with kick"); + } + previousReserves = currentReserves; + } + +} \ No newline at end of file diff --git a/tests/forge/ERC20Pool/invariants/TestBase.sol b/tests/forge/ERC20Pool/invariants/TestBase.sol new file mode 100644 index 000000000..1e626e676 --- /dev/null +++ b/tests/forge/ERC20Pool/invariants/TestBase.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity 0.8.14; + +import '@std/Test.sol'; +import "forge-std/console.sol"; + +import { ERC20Pool } from 'src/ERC20Pool.sol'; +import { ERC20PoolFactory } from 'src/ERC20PoolFactory.sol'; +import { Token } from '../../utils/Tokens.sol'; +import { PoolInfoUtils } from 'src/PoolInfoUtils.sol'; +import { InvariantTest } from './InvariantTest.sol'; + +contract TestBase is InvariantTest, Test { + + // Mainnet ajna address + address internal _ajna = 0x9a96ec9B57Fb64FbC60B423d1f4da7691Bd35079; + + Token internal _quote; + Token internal _collateral; + + ERC20Pool internal _pool; + ERC20Pool internal _impl; + PoolInfoUtils internal _poolInfo; + ERC20PoolFactory internal _poolFactory; + + function setUp() public virtual { + // Tokens + _quote = new Token("Quote", "Q"); + _collateral = new Token("Collateral", "C"); + + // Pool + _poolFactory = new ERC20PoolFactory(_ajna); + _pool = ERC20Pool(_poolFactory.deployPool(address(_collateral), address(_quote), 0.05 * 10**18)); + _poolInfo = new PoolInfoUtils(); + _impl = _poolFactory.implementation(); + } +} \ No newline at end of file diff --git a/tests/forge/ERC20Pool/invariants/handlers/BaseHandler.sol b/tests/forge/ERC20Pool/invariants/handlers/BaseHandler.sol new file mode 100644 index 000000000..863ece656 --- /dev/null +++ b/tests/forge/ERC20Pool/invariants/handlers/BaseHandler.sol @@ -0,0 +1,153 @@ + +// SPDX-License-Identifier: UNLICENSED + +pragma solidity 0.8.14; + +import { Strings } from '@openzeppelin/contracts/utils/Strings.sol'; +import '@std/Test.sol'; +import '@std/Vm.sol'; +import "forge-std/console.sol"; + +import { ERC20Pool } from 'src/ERC20Pool.sol'; +import { ERC20PoolFactory } from 'src/ERC20PoolFactory.sol'; +import { Token } from '../../../utils/Tokens.sol'; +import { PoolInfoUtils } from 'src/PoolInfoUtils.sol'; +import { InvariantTest } from '../InvariantTest.sol'; + + +uint256 constant LENDER_MIN_BUCKET_INDEX = 2570; +uint256 constant LENDER_MAX_BUCKET_INDEX = 2590; + +uint256 constant BORROWER_MIN_BUCKET_INDEX = 2600; +uint256 constant BORROWER_MAX_BUCKET_INDEX = 2620; + +contract BaseHandler is InvariantTest, Test { + + // Tokens + Token internal _quote; + Token internal _collateral; + + // Pool + ERC20Pool internal _pool; + PoolInfoUtils internal _poolInfo; + + // Modifiers + address internal _actor; + uint256 internal _lenderBucketIndex; + uint256 internal _limitIndex; + address[] public actors; + + // Logging + mapping(bytes32 => uint256) public numberOfCalls; + + // Lender tracking + mapping(address => uint256[]) public touchedBuckets; + + // bucket exchange rate invariant check + bool public shouldExchangeRateChange; + + bool public shouldReserveChange; + + // if take is called on auction first time + bool public firstTake; + + // mapping borrower address to first take on auction + mapping(address => bool) internal isFirstTakeOnAuction; + + // amount of reserve increase after first take + uint256 public firstTakeIncreaseInReserve; + + // amount of reserve increase after kicking a loan + uint256 public loanKickIncreaseInReserve; + + constructor(address pool, address quote, address collateral, address poolInfo, uint256 numOfActors) { + // Tokens + _quote = Token(quote); + _collateral = Token(collateral); + + // Pool + _pool = ERC20Pool(pool); + _poolInfo = PoolInfoUtils(poolInfo); + + // Actors + actors = _buildActors(numOfActors); + } + + /**************************************************************************************************************************************/ + /*** Helper Functions ***/ + /**************************************************************************************************************************************/ + + modifier useRandomActor(uint256 actorIndex) { + // pre condition + firstTakeIncreaseInReserve = 0; + loanKickIncreaseInReserve = 0; + + vm.stopPrank(); + + address actor = actors[constrictToRange(actorIndex, 0, actors.length - 1)]; + _actor = actor; + vm.startPrank(actor); + _; + vm.stopPrank(); + } + + modifier useRandomLenderBucket(uint256 bucketIndex) { + uint256[] storage lenderBucketIndexes = touchedBuckets[_actor]; + if (lenderBucketIndexes.length < 3) { + // if actor has touched less than three buckets, add a new bucket + _lenderBucketIndex = constrictToRange(bucketIndex, LENDER_MIN_BUCKET_INDEX, LENDER_MAX_BUCKET_INDEX); + lenderBucketIndexes.push(_lenderBucketIndex); + } else { + // if actor has touched more than three buckets, reuse one of the touched buckets + _lenderBucketIndex = lenderBucketIndexes[constrictToRange(bucketIndex, 0, lenderBucketIndexes.length - 1)]; + } + _; + } + + function _buildActors(uint256 noOfActors_) internal returns(address[] memory) { + address[] memory actorsAddress = new address[](noOfActors_); + for(uint i = 0; i < noOfActors_; i++) { + address actor = makeAddr(string(abi.encodePacked("Actor", Strings.toString(i)))); + actorsAddress[i] = actor; + + vm.startPrank(actor); + + _quote.mint(actor, 1e45); + _quote.approve(address(_pool), 1e45); + + _collateral.mint(actor, 1e45); + _collateral.approve(address(_pool), 1e45); + + vm.stopPrank(); + } + return actorsAddress; + } + + function getActorsCount() external view returns(uint256) { + return actors.length; + } + + function constrictToRange( + uint256 x, + uint256 min, + uint256 max + ) pure public returns (uint256 result) { + require(max >= min, "MAX_LESS_THAN_MIN"); + + uint256 size = max - min; + + if (size == 0) return min; // Using max would be equivalent as well. + if (max != type(uint256).max) size++; // Make the max inclusive. + + // Ensure max is inclusive in cases where x != 0 and max is at uint max. + if (max == type(uint256).max && x != 0) x--; // Accounted for later. + + if (x < min) x += size * (((min - x) / size) + 1); + + result = min + ((x - min) % size); + + // Account for decrementing x to make max inclusive. + if (max == type(uint256).max && x != 0) result++; + } + +} \ No newline at end of file diff --git a/tests/forge/ERC20Pool/invariants/handlers/BasicPoolHandler.sol b/tests/forge/ERC20Pool/invariants/handlers/BasicPoolHandler.sol new file mode 100644 index 000000000..64cbe6558 --- /dev/null +++ b/tests/forge/ERC20Pool/invariants/handlers/BasicPoolHandler.sol @@ -0,0 +1,353 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.14; + +import { Strings } from '@openzeppelin/contracts/utils/Strings.sol'; +import "forge-std/console.sol"; +import '@std/Test.sol'; +import '@std/Vm.sol'; + +import { ERC20Pool } from 'src/ERC20Pool.sol'; +import { ERC20PoolFactory } from 'src/ERC20PoolFactory.sol'; +import { Token } from '../../../utils/Tokens.sol'; +import { PoolInfoUtils, _collateralization } from 'src/PoolInfoUtils.sol'; + +import { LENDER_MIN_BUCKET_INDEX, LENDER_MAX_BUCKET_INDEX, BORROWER_MIN_BUCKET_INDEX, BaseHandler } from './BaseHandler.sol'; + +/** + * @dev this contract manages multiple lenders + * @dev methods in this contract are called in random order + * @dev randomly selects a lender contract to make a txn + */ +abstract contract UnboundedBasicPoolHandler is BaseHandler { + + /**************************************************************************************************************************************/ + /*** Lender Functions ***/ + /**************************************************************************************************************************************/ + + function addQuoteToken(uint256 amount, uint256 bucketIndex) internal { + numberOfCalls['UBBasicHandler.addQuoteToken']++; + + shouldExchangeRateChange = false; + shouldReserveChange = false; + + // Pre condition + (uint256 lpBalanceBefore, ) = _pool.lenderInfo(bucketIndex, _actor); + + _pool.addQuoteToken(amount, bucketIndex, block.timestamp + 1 minutes); + + // Post condition + (uint256 lpBalanceAfter, ) = _pool.lenderInfo(bucketIndex, _actor); + require(lpBalanceAfter > lpBalanceBefore, "LP balance should increase"); + } + + function removeQuoteToken(uint256 amount, uint256 bucketIndex) internal { + numberOfCalls['UBBasicHandler.removeQuoteToken']++; + + // Pre condition + (uint256 lpBalanceBefore, ) = _pool.lenderInfo(bucketIndex, _actor); + + if (lpBalanceBefore == 0) { + amount = constrictToRange(amount, 1, 1e36); + addQuoteToken(amount, bucketIndex); + } + + (lpBalanceBefore, ) = _pool.lenderInfo(bucketIndex, _actor); + + try _pool.removeQuoteToken(amount, bucketIndex) { + // Post condition + (uint256 lpBalanceAfter, ) = _pool.lenderInfo(bucketIndex, _actor); + require(lpBalanceAfter < lpBalanceBefore, "LP balance should decrease"); + shouldExchangeRateChange = false; + shouldReserveChange = false; + } + catch (bytes memory _err){ + bytes32 err = keccak256(_err); + require( + err == keccak256(abi.encodeWithSignature("LUPBelowHTP()")) || + err == keccak256(abi.encodeWithSignature("InsufficientLiquidity()")) || + err == keccak256(abi.encodeWithSignature("RemoveDepositLockedByAuctionDebt()")) || + err == keccak256(abi.encodeWithSignature("NoClaim()"))); + } + } + + function moveQuoteToken(uint256 amount, uint256 fromIndex, uint256 toIndex) internal { + if(fromIndex == toIndex) return; + + (uint256 lpBalance, ) = _pool.lenderInfo(fromIndex, _actor); + + if (lpBalance == 0) { + addQuoteToken(amount, fromIndex); + } + + try _pool.moveQuoteToken(amount, fromIndex, toIndex, block.timestamp + 1 minutes) { + shouldExchangeRateChange = false; + shouldReserveChange = false; + } + catch (bytes memory _err){ + bytes32 err = keccak256(_err); + require( + err == keccak256(abi.encodeWithSignature("LUPBelowHTP()")) || + err == keccak256(abi.encodeWithSignature("InsufficientLiquidity()")) || + err == keccak256(abi.encodeWithSignature("MoveToSamePrice()")) || + err == keccak256(abi.encodeWithSignature("DustAmountNotExceeded()")) || + err == keccak256(abi.encodeWithSignature("InvalidIndex()")) || + err == keccak256(abi.encodeWithSignature("BucketBankruptcyBlock()")) + ); + } + } + + function addCollateral(uint256 amount, uint256 bucketIndex) internal { + numberOfCalls['UBBasicHandler.addCollateral']++; + + shouldExchangeRateChange = false; + shouldReserveChange = false; + + // Pre condition + (uint256 lpBalanceBefore, ) = _pool.lenderInfo(bucketIndex, _actor); + + _pool.addCollateral(amount, bucketIndex, block.timestamp + 1 minutes); + + // Post condition + (uint256 lpBalanceAfter, ) = _pool.lenderInfo(bucketIndex, _actor); + require(lpBalanceAfter > lpBalanceBefore, "LP balance should increase"); + } + + function removeCollateral(uint256 amount, uint256 bucketIndex) internal { + numberOfCalls['UBBasicHandler.removeCollateral']++; + + shouldExchangeRateChange = false; + shouldReserveChange = false; + + // Pre condition + (uint256 lpBalanceBefore, ) = _pool.lenderInfo(bucketIndex, _actor); + + if(lpBalanceBefore == 0) { + addCollateral(amount, bucketIndex); + } + + (lpBalanceBefore, ) = _pool.lenderInfo(bucketIndex, _actor); + + _pool.removeCollateral(amount, bucketIndex); + + // Post condition + (uint256 lpBalanceAfter, ) = _pool.lenderInfo(bucketIndex, _actor); + require(lpBalanceAfter < lpBalanceBefore, "LP balance should decrease"); + } + + /**************************/ + /*** Borrower Functions ***/ + /**************************/ + + function pledgeCollateral(uint256 amount) internal { + numberOfCalls['UBBasicHandler.pledgeCollateral']++; + + shouldExchangeRateChange = false; + shouldReserveChange = false; + + _pool.drawDebt(_actor, 0, 0, amount); + } + + function pullCollateral(uint256 amount) internal { + numberOfCalls['UBBasicHandler.pullCollateral']++; + + try _pool.repayDebt(_actor, 0, amount, _actor, 7388) { + shouldExchangeRateChange = false; + shouldReserveChange = false; + } catch (bytes memory _err){ + bytes32 err = keccak256(_err); + require(err == keccak256(abi.encodeWithSignature("InsufficientCollateral()"))); + } + } + + function drawDebt(uint256 amount) internal { + numberOfCalls['UBBasicHandler.drawDebt']++; + + // Pre Condition + // 1. borrower's debt should exceed minDebt + // 2. pool needs sufficent quote token to draw debt + // 3. drawDebt should not make borrower under collateralized + + // 1. borrower's debt should exceed minDebt + (uint256 debt, uint256 collateral, ) = _poolInfo.borrowerInfo(address(_pool), _actor); + (uint256 minDebt, , , ) = _poolInfo.poolUtilizationInfo(address(_pool)); + if (amount < minDebt) amount = minDebt + 1; + + + // TODO: Need to constrain amount so LUP > HTP + + + // 2. pool needs sufficent quote token to draw debt + uint256 poolQuoteBalance = _quote.balanceOf(address(_pool)); + + if (amount > poolQuoteBalance) { + addQuoteToken(amount * 2, LENDER_MAX_BUCKET_INDEX); + } + + // 3. drawing of addition debt will make them under collateralized + uint256 lup = _poolInfo.lup(address(_pool)); + (debt, collateral, ) = _poolInfo.borrowerInfo(address(_pool), _actor); + + if (_collateralization(debt, collateral, lup) < 1) { + repayDebt(debt); + (debt, collateral, ) = _poolInfo.borrowerInfo(address(_pool), _actor); + require(debt == 0, "borrower has debt"); + } + + (uint256 poolDebt, , ) = _pool.debtInfo(); + + // find bucket to borrow quote token + uint256 bucket = _pool.depositIndex(amount + poolDebt) - 1; + + uint256 price = _poolInfo.indexToPrice(bucket); + + uint256 collateralToPledge = ((amount * 1e18 + price / 2) / price) * 101 / 100; + + try _pool.drawDebt(_actor, amount, 7388, collateralToPledge) { + shouldExchangeRateChange = true; + shouldReserveChange = true; + } + catch (bytes memory _err){ + bytes32 err = keccak256(_err); + require(err == keccak256(abi.encodeWithSignature("BorrowerUnderCollateralized()"))); + } + } + + function repayDebt(uint256 amountToRepay) internal { + numberOfCalls['UBBasicHandler.repayDebt']++; + + // Pre condition + (uint256 debt, , ) = PoolInfoUtils(_poolInfo).borrowerInfo(address(_pool), _actor); + if (debt == 0) { + drawDebt(amountToRepay); + } + + try _pool.repayDebt(_actor, amountToRepay, 0, _actor, 7388) { + shouldExchangeRateChange = true; + shouldReserveChange = true; + } + catch(bytes memory _err) { + bytes32 err = keccak256(_err); + require( + err == keccak256(abi.encodeWithSignature("NoDebt()")) || + err == keccak256(abi.encodeWithSignature("AmountLTMinDebt()")) + ); + } + } + +} + + +/** + * @dev this contract manages multiple lenders + * @dev methods in this contract are called in random order + * @dev randomly selects a lender contract to make a txn + */ +contract BasicPoolHandler is UnboundedBasicPoolHandler { + + constructor(address pool, address quote, address collateral, address poolInfo, uint256 numOfActors) BaseHandler(pool, quote, collateral, poolInfo, numOfActors) {} + + /**************************/ + /*** Lender Functions ***/ + /**************************/ + + function addQuoteToken(uint256 actorIndex, uint256 amount, uint256 bucketIndex) public useRandomActor(actorIndex) useRandomLenderBucket(bucketIndex) { + numberOfCalls['BBasicHandler.addQuoteToken']++; + + uint256 totalSupply = _quote.totalSupply(); + uint256 minDeposit = totalSupply == 0 ? 1 : _quote.balanceOf(address(_actor)) / totalSupply + 1; + amount = constrictToRange(amount, minDeposit, 1e36); + + // Action + super.addQuoteToken(amount, _lenderBucketIndex); + } + + function removeQuoteToken(uint256 actorIndex, uint256 amount, uint256 bucketIndex) public useRandomActor(actorIndex) useRandomLenderBucket(bucketIndex) { + numberOfCalls['BBasicHandler.removeQuoteToken']++; + + uint256 poolBalance = _quote.balanceOf(address(_pool)); + + if (poolBalance < amount) return; // (not enough quote token to withdraw / quote tokens are borrowed) + + // Action + super.removeQuoteToken(amount, _lenderBucketIndex); + } + + function moveQuoteToken(uint256 actorIndex, uint256 amount, uint256 fromBucketIndex, uint256 toBucketIndex) public useRandomActor(actorIndex) { + numberOfCalls['BBasicHandler.moveQuoteToken']++; + + fromBucketIndex = constrictToRange(fromBucketIndex, LENDER_MIN_BUCKET_INDEX, LENDER_MAX_BUCKET_INDEX); + + toBucketIndex = constrictToRange(toBucketIndex, LENDER_MIN_BUCKET_INDEX, LENDER_MAX_BUCKET_INDEX); + + amount = constrictToRange(amount, 1, 1e36); + + super.moveQuoteToken(amount, fromBucketIndex, toBucketIndex); + } + + function addCollateral(uint256 actorIndex, uint256 amount, uint256 bucketIndex) public useRandomActor(actorIndex) useRandomLenderBucket(bucketIndex) { + numberOfCalls['BBasicHandler.addCollateral']++; + + amount = constrictToRange(amount, 1, 1e36); + + // Action + super.addCollateral(amount, _lenderBucketIndex); + } + + function removeCollateral(uint256 actorIndex, uint256 amount, uint256 bucketIndex) public useRandomActor(actorIndex) useRandomLenderBucket(bucketIndex) { + numberOfCalls['BBasicHandler.removeCollateral']++; + + (uint256 lpBalance, ) = _pool.lenderInfo(_lenderBucketIndex, _actor); + ( , uint256 bucketCollateral, , , ) = _pool.bucketInfo(_lenderBucketIndex); + + if (lpBalance == 0 || bucketCollateral == 0) return; // no value in bucket + + amount = constrictToRange(amount, 1, 1e36); + + // Action + super.removeCollateral(amount, _lenderBucketIndex); + } + + + /**************************/ + /*** Borrower Functions ***/ + /**************************/ + + function pledgeCollateral(uint256 actorIndex, uint256 amountToPledge) public useRandomActor(actorIndex) { + numberOfCalls['BBasicHandler.pledgeCollateral']++; + + amountToPledge = constrictToRange(amountToPledge, 1, 1e36); + + // Action + super.pledgeCollateral(amountToPledge); + } + + function pullCollateral(uint256 actorIndex, uint256 amountToPull) public useRandomActor(actorIndex) { + numberOfCalls['BBasicHandler.pullCollateral']++; + + amountToPull = constrictToRange(amountToPull, 1, 1e36); + + // Action + super.pullCollateral(amountToPull); + } + + function drawDebt(uint256 actorIndex, uint256 amountToBorrow) public useRandomActor(actorIndex) { + numberOfCalls['BBasicHandler.drawDebt']++; + + amountToBorrow = constrictToRange(amountToBorrow, 1, 1e36); + + // Action + super.drawDebt(amountToBorrow); + + // skip time to make borrower undercollateralized + vm.warp(block.timestamp + 200 days); + } + + function repayDebt(uint256 actorIndex, uint256 amountToRepay) public useRandomActor(actorIndex) { + numberOfCalls['BBasicHandler.repayDebt']++; + + amountToRepay = constrictToRange(amountToRepay, 1, 1e36); + + // Action + super.repayDebt(amountToRepay); + } +} diff --git a/tests/forge/ERC20Pool/invariants/handlers/IBaseHandler.sol b/tests/forge/ERC20Pool/invariants/handlers/IBaseHandler.sol new file mode 100644 index 000000000..092c21f7e --- /dev/null +++ b/tests/forge/ERC20Pool/invariants/handlers/IBaseHandler.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity 0.8.14; + +interface IBaseHandler { + + function getActorsCount() external view returns(uint256); + + function actors(uint256) external view returns(address); + + function numberOfCalls(bytes32) external view returns(uint256); + + function shouldExchangeRateChange() external view returns(bool); + + function shouldReserveChange() external view returns(bool); + + function firstTake() external view returns(bool); + + function firstTakeIncreaseInReserve() external view returns(uint256); + + function loanKickIncreaseInReserve() external view returns(uint256); +} \ No newline at end of file diff --git a/tests/forge/ERC20Pool/invariants/handlers/LiquidationPoolHandler.sol b/tests/forge/ERC20Pool/invariants/handlers/LiquidationPoolHandler.sol new file mode 100644 index 000000000..3c3bdc319 --- /dev/null +++ b/tests/forge/ERC20Pool/invariants/handlers/LiquidationPoolHandler.sol @@ -0,0 +1,148 @@ + +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.14; + +import '@std/Vm.sol'; + +import { BasicPoolHandler } from './BasicPoolHandler.sol'; +import { LENDER_MIN_BUCKET_INDEX, LENDER_MAX_BUCKET_INDEX, BaseHandler } from './BaseHandler.sol'; +import { Maths } from 'src/libraries/internal/Maths.sol'; + +abstract contract UnBoundedLiquidationPoolHandler is BaseHandler { + function kickAuction(address borrower) internal { + numberOfCalls['UBLiquidationHandler.kickAuction']++; + + (uint256 borrowerDebt, , ) = _poolInfo.borrowerInfo(address(_pool), borrower); + + try _pool.kick(borrower) { + shouldExchangeRateChange = true; + shouldReserveChange = true; + loanKickIncreaseInReserve = Maths.wmul(borrowerDebt, 0.25 * 1e18); + } + catch { + } + } + + function takeAuction(address borrower, uint256 amount, address taker) internal { + numberOfCalls['UBLiquidationHandler.takeAuction']++; + + (uint256 borrowerDebt, , ) = _poolInfo.borrowerInfo(address(_pool), borrower); + + try _pool.take(borrower, amount, taker, bytes("")) { + shouldExchangeRateChange = true; + shouldReserveChange = true; + + if(!isFirstTakeOnAuction[borrower]) { + firstTakeIncreaseInReserve = Maths.wmul(borrowerDebt, 0.07 * 1e18); + firstTake = true; + isFirstTakeOnAuction[borrower] = true; + } + else { + isFirstTakeOnAuction[borrower] = false; + firstTake = false; + } + } + catch { + } + } + + function bucketTake(address borrower, bool depositTake, uint256 bucketIndex) internal { + numberOfCalls['UBLiquidationHandler.bucketTake']++; + + (uint256 borrowerDebt, , ) = _poolInfo.borrowerInfo(address(_pool), borrower); + + try _pool.bucketTake(borrower, depositTake, bucketIndex) { + shouldExchangeRateChange = true; + shouldReserveChange = true; + + if(!firstTake) { + firstTakeIncreaseInReserve = Maths.wmul(borrowerDebt, 0.07 * 1e18); + firstTake = true; + } + else { + firstTake = false; + } + } + catch { + } + } +} + +contract LiquidationPoolHandler is UnBoundedLiquidationPoolHandler, BasicPoolHandler { + + constructor(address pool, address quote, address collateral, address poolInfo, uint256 numOfActors) BasicPoolHandler(pool, quote, collateral, poolInfo, numOfActors) {} + + function _kickAuction(uint256 borrowerIndex, uint256 amount, uint256 kickerIndex) internal useRandomActor(kickerIndex) { + numberOfCalls['BLiquidationHandler.kickAuction']++; + + shouldExchangeRateChange = true; + + borrowerIndex = constrictToRange(borrowerIndex, 0, actors.length - 1); + address borrower = actors[borrowerIndex]; + address kicker = _actor; + amount = constrictToRange(amount, 1, 1e36); + + ( , , , uint256 kickTime, , , , , ) = _pool.auctionInfo(borrower); + + if (kickTime == 0) { + (uint256 debt, , ) = _pool.borrowerInfo(borrower); + if (debt == 0) { + changePrank(borrower); + _actor = borrower; + super.drawDebt(amount); + } + changePrank(kicker); + _actor = kicker; + super.kickAuction(borrower); + } + + // skip some time for more interest + vm.warp(block.timestamp + 2 hours); + } + + function kickAuction(uint256 borrowerIndex, uint256 amount, uint256 kickerIndex) external { + _kickAuction(borrowerIndex, amount, kickerIndex); + } + + function takeAuction(uint256 borrowerIndex, uint256 amount, uint256 actorIndex) external useRandomActor(actorIndex){ + numberOfCalls['BLiquidationHandler.takeAuction']++; + + amount = constrictToRange(amount, 1, 1e36); + + shouldExchangeRateChange = true; + + borrowerIndex = constrictToRange(borrowerIndex, 0, actors.length - 1); + + address borrower = actors[borrowerIndex]; + address taker = _actor; + + ( , , , uint256 kickTime, , , , , ) = _pool.auctionInfo(borrower); + + if (kickTime == 0) { + _kickAuction(borrowerIndex, amount * 100, actorIndex); + } + changePrank(taker); + super.takeAuction(borrower, amount, taker); + } + + function bucketTake(uint256 borrowerIndex, uint256 bucketIndex, bool depositTake, uint256 takerIndex) external useRandomActor(takerIndex) { + numberOfCalls['BLiquidationHandler.bucketTake']++; + + shouldExchangeRateChange = true; + + borrowerIndex = constrictToRange(borrowerIndex, 0, actors.length - 1); + + bucketIndex = constrictToRange(bucketIndex, LENDER_MIN_BUCKET_INDEX, LENDER_MAX_BUCKET_INDEX); + + address borrower = actors[borrowerIndex]; + address taker = _actor; + + ( , , , uint256 kickTime, , , , , ) = _pool.auctionInfo(borrower); + + if (kickTime == 0) { + _kickAuction(borrowerIndex, 1e24, bucketIndex); + } + changePrank(taker); + super.bucketTake(borrower, depositTake, bucketIndex); + } +} \ No newline at end of file diff --git a/tests/forge/ERC20Pool/invariants/handlers/ReservePoolHandler.sol b/tests/forge/ERC20Pool/invariants/handlers/ReservePoolHandler.sol new file mode 100644 index 000000000..1e592a2e3 --- /dev/null +++ b/tests/forge/ERC20Pool/invariants/handlers/ReservePoolHandler.sol @@ -0,0 +1,48 @@ + +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.14; + +import '@std/Vm.sol'; + +import { LiquidationPoolHandler } from './LiquidationPoolHandler.sol'; +import { LENDER_MIN_BUCKET_INDEX, LENDER_MAX_BUCKET_INDEX, BaseHandler } from './BaseHandler.sol'; +import { Auctions } from 'src/libraries/external/Auctions.sol'; + +abstract contract UnBoundedReservePoolHandler is BaseHandler { + function startClaimableReserveAuction() internal { + (, uint256 claimableReserves, , , ) = _poolInfo.poolReservesInfo(address(_pool)); + if(claimableReserves == 0) return; + try _pool.startClaimableReserveAuction(){ + shouldReserveChange = true; + } catch { + } + } + + function takeReserves(uint256 amount) internal { + try _pool.takeReserves(amount){ + shouldReserveChange = true; + } catch { + } + } +} + +contract ReservePoolHandler is UnBoundedReservePoolHandler, LiquidationPoolHandler { + + constructor(address pool, address quote, address collateral, address poolInfo, uint256 numOfActors) LiquidationPoolHandler(pool, quote, collateral, poolInfo, numOfActors) {} + + function startClaimableReserveAuction(uint256 actorIndex) external useRandomActor(actorIndex) { + super.startClaimableReserveAuction(); + } + + function takeReserves(uint256 actorIndex, uint256 amount) external useRandomActor(actorIndex) { + (, , uint256 claimableReservesRemaining, , ) = _poolInfo.poolReservesInfo(address(_pool)); + + if(claimableReservesRemaining == 0) { + super.startClaimableReserveAuction(); + } + (, , claimableReservesRemaining, , ) = _poolInfo.poolReservesInfo(address(_pool)); + + amount = constrictToRange(amount, 0, claimableReservesRemaining); + super.takeReserves(amount); + } +} \ No newline at end of file diff --git a/tests/forge/ERC20Pool/regression/RegressionTestBasic.t.sol b/tests/forge/ERC20Pool/regression/RegressionTestBasic.t.sol new file mode 100644 index 000000000..57e0a9819 --- /dev/null +++ b/tests/forge/ERC20Pool/regression/RegressionTestBasic.t.sol @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity 0.8.14; + +import { BasicInvariants } from "../invariants/BasicInvariants.t.sol"; + +import '@std/console.sol'; + +contract RegressionTestBasic is BasicInvariants { + + function setUp() public override { + super.setUp(); + } + + function test_regression_invariantUnderflow_1() external { + _basicPoolHandler.addQuoteToken(14227, 5211, 3600000000000000000000); + // check invariants hold true + invariant_Lps_B1(); + invariant_quoteTokenBalance_QT1(); + } + + function test_exchange_rate_bug_simulation() external { + // Action sequence + // 1. addQuoteToken(6879, 2570) + // 2. addCollateral(3642907759282013932739218713, 2570) + // 3. removeCollateral(296695924278944779257290397234298756, 2570) + + uint256 previousExchangeRate = 1e18; + _basicPoolHandler.addQuoteToken(999999999844396154169639088436193915956854451, 6879, 2809); + ( , uint256 quote, uint256 collateral, uint256 lps, , uint256 exchangeRate) = _poolInfo.bucketInfo(address(_pool), 2570); + console.log("After addQuoteToken(6879, 2570)"); + console.log("============"); + console.log("Quote Tokens -->", quote); + console.log("Collateral Tokens -->", collateral); + console.log("Lps -->", lps); + console.log("Exchange Rate-->", exchangeRate); + console.log("============"); + require(previousExchangeRate == exchangeRate, "Incorrect exchange rate"); + previousExchangeRate = exchangeRate; + _basicPoolHandler.addCollateral(2, 36429077592820139327392187131, 202214962129783771592); + ( , quote, collateral, lps, , exchangeRate) = _poolInfo.bucketInfo(address(_pool), 2570); + console.log("After addCollateral(3642907759282013932739218713, 2570)"); + console.log("============"); + console.log("Quote Tokens -->", quote); + console.log("Collateral Tokens -->", collateral); + console.log("Lps -->", lps); + console.log("Exchange Rate-->", exchangeRate); + console.log("============"); + require(previousExchangeRate == exchangeRate, "Incorrect exchange rate"); + previousExchangeRate = exchangeRate; + _basicPoolHandler.removeCollateral(1, 2296695924278944779257290397234298756, 10180568736759156593834642286260647915348262280903719122483474452532722106636); + ( , quote, collateral, lps, , exchangeRate) = _poolInfo.bucketInfo(address(_pool), 2570); + console.log("After removeCollateral(296695924278944779257290397234298756, 2570)"); + console.log("============"); + console.log("Quote Tokens -->", quote); + console.log("Collateral Tokens -->", collateral); + console.log("Lps -->", lps); + console.log("Exchange Rate-->", exchangeRate); + console.log("============"); + require(previousExchangeRate == exchangeRate, "Incorrect exchange rate"); + } + + function test_exchange_rate_bug2() external { + uint256 previousExchangeRate = 1e18; + _basicPoolHandler.addQuoteToken(211670885988646987334214990781526025942, 115792089237316195423570985008687907853269984665640564039457584007913129639934, 6894274025938223490357894120267612065037086600750070030707794233); + + ( , uint256 quote, uint256 collateral, uint256 lps, , uint256 exchangeRate) = _poolInfo.bucketInfo(address(_pool), 2570); + console.log("After addQuoteToken(211670885988646987334214990781526025942, 115792089237316195423570985008687907853269984665640564039457584007913129639934, 6894274025938223490357894120267612065037086600750070030707794233)"); + console.log("============"); + console.log("Quote Tokens -->", quote); + console.log("Collateral Tokens -->", collateral); + console.log("Lps -->", lps); + console.log("Exchange Rate-->", exchangeRate); + console.log("============"); + require(previousExchangeRate == exchangeRate, "Incorrect exchange rate"); + previousExchangeRate = exchangeRate; + _basicPoolHandler.addCollateral(117281, 115792089237316195423570985008687907853269984665640564039457584007913129639935, 2); + + ( , quote, collateral, lps, , exchangeRate) = _poolInfo.bucketInfo(address(_pool), 2570); + console.log("After addCollateral(117281, 115792089237316195423570985008687907853269984665640564039457584007913129639935, 2)"); + console.log("============"); + console.log("Quote Tokens -->", quote); + console.log("Collateral Tokens -->", collateral); + console.log("Lps -->", lps); + console.log("Exchange Rate-->", exchangeRate); + console.log("============"); + require(previousExchangeRate == exchangeRate, "Incorrect exchange rate"); + previousExchangeRate = exchangeRate; + + _basicPoolHandler.removeCollateral(115792089237316195423570985008687907853269984665640564039457584007913129639932, 12612911637698029036253737442696522, 115792089237316195423570985008687907853269984665640564039457584007913129639933); + + ( , quote, collateral, lps, , exchangeRate) = _poolInfo.bucketInfo(address(_pool), 2570); + console.log("After removeCollateral(115792089237316195423570985008687907853269984665640564039457584007913129639932, 12612911637698029036253737442696522, 115792089237316195423570985008687907853269984665640564039457584007913129639933)"); + console.log("============"); + console.log("Quote Tokens -->", quote); + console.log("Collateral Tokens -->", collateral); + console.log("Lps -->", lps); + console.log("Exchange Rate-->", exchangeRate); + console.log("============"); + // require(previousExchangeRate == exchangeRate, "Incorrect exchange rate"); + previousExchangeRate = exchangeRate; + + _basicPoolHandler.removeCollateral(1, 1e36, 2570); + _basicPoolHandler.removeQuoteToken(1, 1e36, 2570); + + _basicPoolHandler.removeCollateral(2, 1e36, 2570); + _basicPoolHandler.removeQuoteToken(2, 1e36, 2570); + + ( , quote, collateral, lps, , exchangeRate) = _poolInfo.bucketInfo(address(_pool), 2570); + console.log("After removeCollateral(115792089237316195423570985008687907853269984665640564039457584007913129639932, 12612911637698029036253737442696522, 115792089237316195423570985008687907853269984665640564039457584007913129639933)"); + console.log("============"); + console.log("Quote Tokens -->", quote); + console.log("Collateral Tokens -->", collateral); + console.log("Lps -->", lps); + console.log("Exchange Rate-->", exchangeRate); + console.log("============"); + require(previousExchangeRate == exchangeRate, "Incorrect exchange rate"); + + } + +} \ No newline at end of file diff --git a/tests/forge/ERC20Pool/regression/RegressionTestLiquidation.t.sol b/tests/forge/ERC20Pool/regression/RegressionTestLiquidation.t.sol new file mode 100644 index 000000000..c9a4facdf --- /dev/null +++ b/tests/forge/ERC20Pool/regression/RegressionTestLiquidation.t.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity 0.8.14; + +import { LiquidationInvariant } from "../invariants/LiquidationInvariant.t.sol"; + +import '@std/console.sol'; + +contract RegressionTestLiquidation is LiquidationInvariant { + + function setUp() public override { + super.setUp(); + + } + + function test_regression_quote_token() external { + _liquidationPoolHandler.addQuoteToken(115792089237316195423570985008687907853269984665640564039457584007913129639932, 3, 115792089237316195423570985008687907853269984665640564039457584007913129639932); + + // check invariants hold true + invariant_quoteTokenBalance_QT1(); + } + + function test_arithmetic_overflow() external { + _liquidationPoolHandler.kickAuction(128942392769655840156268259377571235707684499808935108685525899532745, 9654010200996517229486923829624352823010316518405842367464881, 135622574118732106350824249104903); + _liquidationPoolHandler.addQuoteToken(3487, 871, 1654); + + // check invariants hold true + invariant_quoteTokenBalance_QT1(); + } + + function test_bucket_take_lps_bug() public { + _liquidationPoolHandler.removeQuoteToken(7033457611004217223271238592369692530886316746601644, 0, 115792089237316195423570985008687907853269984665640564039457584007913129639932); + _liquidationPoolHandler.addQuoteToken(1, 20033186019073, 1); + _liquidationPoolHandler.bucketTake(0, 0, false, 2876997751); + + invariant_Lps_B1(); + } + + function test_interest_rate_bug() public { + _liquidationPoolHandler.bucketTake(18065045387666484532028539614323078235438354477798625297386607289, 14629545458306, true, 1738460279262663206365845078188769); + + invariant_interest_rate_I1(); + } +} \ No newline at end of file diff --git a/tests/forge/ERC20Pool/regression/RegressionTestReserves.t.sol b/tests/forge/ERC20Pool/regression/RegressionTestReserves.t.sol new file mode 100644 index 000000000..53ed71d77 --- /dev/null +++ b/tests/forge/ERC20Pool/regression/RegressionTestReserves.t.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity 0.8.14; + +import { ReserveInvariants } from "../invariants/ReserveInvariants.t.sol"; + +import '@std/console.sol'; + +contract RegressionTestReserve is ReserveInvariants { + + function setUp() public override { + super.setUp(); + } + + function _test_reserve_1() external { + (uint256 reserve, , , , ) = _poolInfo.poolReservesInfo(address(_pool)); + console.log("Initial Reserve -->", reserve); + + _reservePoolHandler.kickAuction(3833, 15167, 15812); + + (reserve, , , , ) = _poolInfo.poolReservesInfo(address(_pool)); + console.log("Reserve after kick --->", reserve); + _invariant_reserves_RE1_RE2_RE3_RE4_RE5_RE6_RE7_RE8_RE9(); + + + _reservePoolHandler.removeQuoteToken(3841, 5339, 3672); + + (reserve, , , , ) = _poolInfo.poolReservesInfo(address(_pool)); + console.log("Reserve after removeQuoteToken --->", reserve); + _invariant_reserves_RE1_RE2_RE3_RE4_RE5_RE6_RE7_RE8_RE9(); + } + + function _test_reserve_2() external { + (uint256 reserve, , , , ) = _poolInfo.poolReservesInfo(address(_pool)); + console.log("Initial Reserve -->", reserve); + + _reservePoolHandler.bucketTake(19730, 10740, false, 15745); + + (reserve, , , , ) = _poolInfo.poolReservesInfo(address(_pool)); + console.log("Reserve after bucketTake --->", reserve); + _invariant_reserves_RE1_RE2_RE3_RE4_RE5_RE6_RE7_RE8_RE9(); + + + _reservePoolHandler.addCollateral(14982, 18415, 2079); + + (reserve, , , , ) = _poolInfo.poolReservesInfo(address(_pool)); + console.log("Reserve after addCollateral --->", reserve); + _invariant_reserves_RE1_RE2_RE3_RE4_RE5_RE6_RE7_RE8_RE9(); + } +} \ No newline at end of file