From ec79122645eea6468bd6040f2ad67eab00eae34e Mon Sep 17 00:00:00 2001 From: grandizzy <38490174+grandizzy@users.noreply.github.com> Date: Mon, 13 Feb 2023 05:34:57 +0200 Subject: [PATCH] Sherlock reviewed fixes - to be merged in master only after Sherlock review (#594) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Develop (#565) * reworked the RNG to use a deterministic seed (#523) * reworked the RNG to use a deterministic seed * reordered params such that they are in the same order as "bound" is called * Nonfuzzy fill (#529) * work in progress * reproduced fenwick tree failures at last index * proposed changes in light that max index is special case for prefix suim (#533) * proposed changes in light that max index is special case for prefix suim * removed comment Co-authored-by: mwc Co-authored-by: Ed Noepel * removed two temp tests * ported Matt's changes to the fuzzed implementation Co-authored-by: mattcushman <36414299+mattcushman@users.noreply.github.com> Co-authored-by: mwc Co-authored-by: Ed Noepel Co-authored-by: mattcushman <36414299+mattcushman@users.noreply.github.com> Co-authored-by: mwc Co-authored-by: Ed Noepel * Docs (#535) * Diagrams * Add components * Updates * Center components * add pool architectire diagrams (#536) Co-authored-by: prateek105 Co-authored-by: Prateek Gupta Co-authored-by: prateek105 * Test Coverage and Tests Style improvements (#541) * - Ajna balance less than rewards tests * NFT take tests * Loan heap test coverage * PoolCommons 100% test coverage - no interest earned when htp > max price * Test coverage move dusty amount * Changes after review: - comments - common style on touched tests * Common style across all unit tests * PositionManager and RewardsManager deployment (#547) * replaced bash script with forge script, update for PositionManager and RewardsManager * intentionally broke example addresses so no one tries to use them for anything * CI updates (#549) * testing set-output for size-check * update to cachev3 * make it look like an env var * use it like an env var * revert to old usage syntax * deleted redundant forge test, since code coverage already runs the tests * Made Token limitations more robust (#557) # Description of change ## High level * Altered `README.MD` to reflect a more robust definition of incompatible tokens --------- Co-authored-by: Ed Noepel <46749157+EdNoepel@users.noreply.github.com> Co-authored-by: mattcushman <36414299+mattcushman@users.noreply.github.com> Co-authored-by: mwc Co-authored-by: Ed Noepel Co-authored-by: Prateek Gupta Co-authored-by: prateek105 Co-authored-by: Ian Harvey * Consistent naming, replace lpTokens with LPs (#543) * Consistent naming, replace lpTokens with LPs * Changes after review: replace _lpTokenAllowances with _lpAllowances * Apply test style * gas improvements (#542) * Gas improvements: - LenderActions.removeQuoteToken: reuse depositTime instead loading again from storage * RewardsManager: calculateAndClaimRewards, increment and then use next epoch * Remove duped calculation of toPrice from moveQuoteToken function * Take underflows when full pool debt repaid (#551) Take underflows when full pool debt repaid Scenario is exposed in testTakeAuctionRepaidAmountGreaterThanPoolDebt test: - when auction debt is fully repaid by take action then accrued pool debt value is less than repaid amount with 1 unit of WAD. When the repaid amount is subtracted from pool debt value (normally leaving no debt in pool) the operation underflows. - this happens because repaid debt is calculated using t0 value (t0 repaid debt * inflator) and then subtracted from already calculated pool debt - fix is to calculate pool debt as (t0 pool debt - t0 repaid debt) * inflator, hence preventing rounding issues The scope of this PR is extended to uniformize the t0 / accrued debt values calculation across all contracts by using pattern below: - when values relative to t0 are changed then t0 is updated first and then transformed in actual value (by multiplying it with inflator value) - all accumulations of pool or borrower debt are done on spot and not deferred to Pool contracts (which only save states) * Sherlock 103.md: Remaining collateral used by ERC721Pool is missed in Auctions take and bucketTake return structures (#566) * Nft pledge accumulator (#567) * Add test to expose pledgeCollateral inconsistency when settle NFT auction * Fix pledgedCollateral accumulator * Fix settle auction on bucket take * Add test for auction settle and compensate borrower when settle pool debt. Check pool collateral conistency across buckets * Fix for pledged collateral accumulator when settle auction with regular take * Reorg test, add _assertCollateralInvariants helper * Sherlock 162.md: ERC721Pool taker callback misreports quote funds whenever there was collateral amount rounding (#568) * Flashloan non18 decimals (#569) Fix flashloan, reserve auction and settle auction for non standard collateral / quote token precision Always use token precision when transferring during flashloans (and do not scale amounts to pool precision). For pool reserves use scaled pool token balance (WAD) so the reserves are accurately calculated. * Fix flashloan and reserve auction for non standard collateral / quote token precision * Changes after review: rename _getScaledPoolQuoteTokenBalance to _getNormalizedPoolQuoteTokenBalance * remove redundant code (#558) Co-authored-by: prateek105 * Reduce flashloans logic, introduce _isFlashloanSupported function (default returns true for quote token address, overriden in ERC20 pool to allow both quote and collateral token). Reorg the flashloan reverts conditions * Safety measure: (#588) - setting the remaining collateral to the current by default in drawDebt/repayDebt/_takeLoan functions - update alongside with borrower.colalteral when more collateral pledged Note: the remainingCollateral is used by NFT pool and only in the case of auction settlement. This commit doesn't change any logic but it's a safety measure for future developments that could alter this logic * Sherlock 104.md: Settled collateral of a borrower aren't available for lenders until borrower's debt is fully cleared (#570) - in ERC721Pool settle rebalance token ids if there's collateral settled - remove unused return param from Auction.settle - fix failing test * Sherlock 105.md: ERC721Pool's mergeOrRemoveCollateral allows to remove collateral while auction is clearable (#571) - check _revertIfAuctionClearable in mergeOrRemoveCollateral function - update tests: add _assertMergeRemoveCollateralAuctionNotClearedRevert and assert in ERC721 mergeOrRemove unit tests * Sherlock 101.md: Flashloan end result isn't controlled (#572) - record pool balance before transfer flash loaned ERC20 tokens - compare pool balance after flashloan with the initial / recorded balance - intorduce FlashloanIncorrectBalance error to revert if initial and current pool balances are different - update tests, introduce strategy that transfers tokens to pool, hence increasing pool balance, and expect FlashloanIncorrectBalance revert * Sherlock 183.md: RewardsManager doesn't delete old bucket snapshot info on unstaking (#573) - add getBucketStateStakeInfo view function to return info of recorded bucket at stake time - when unstake loop through positions and delete BucketState structs - in test helper _unstakeToken make sure there's no bucket state info remaining after token unstaked * Sherlock 151.md: Permanent freezing of unclaimed yield (#574) * Sherlock 151.md: Permanent freezing of unclaimed yield - new EpochNotAvailable custom error added - in claimRewards revert if epoch to claim is greater than latest burn epoch - add test * Changes after review: freeze typo * Sherlock 134.md: (#575) reported as Transferring funds to yourself increases your balance actually: Transferring funds to yourself reset balance to 0 (effect is that owner lose all their LPs as it is removed as bucket lender at LenderActions#L550) - added new custom error TransferToSameOwner, revert in Pool.transferLPs if new owner same as owner - added revert test on transfer to same address as owner address - tests style format * Sherlock 075.md: If borrower or kicker got blacklisted by asset contract their collateral or bond funds can be permanently frozen with the pool (#578) - Add recipient arg to withdrawBonds and repayDebt functions - add tests TBD: should we check and revert if recipient is 0x address * Sherlock 139.md (#579) * Sherlock 139.md: scaledQuoteTokenAmount isn't updated to be collateral sell value in the quote token constraint case of _calculateTakeFlowsAndBondChange - setting the `scaledQuoteTokenAmount` to the `C * p` as a final step of quote token amount constraint case - update tests * Added an example to show that the new exchange rate is invariant * Add tearDown to test, remove return --------- Co-authored-by: mwc * Sherlock 096.md: Interest rate for pool is bounded wrongly (#580) - allow creation of pools with min / max rate values - add tests * Sherlock 068.md (#581) * Create standardized PR templates for contracts (#539) This PR creates three github PR templates that should be followed: * `logic_src_changes.md` * `cosmetic_src_changes.md` * `testing_or_misc_nonsrc_changes.md` To learn more about how GH templates work -> https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/creating-a-pull-request-template-for-your-repository Co-authored-by: Ian Harvey * Single template (#546) * merged all of the templates into one so the text will auto fill when a PR is created. * was running into this issue documented here with auto fill -> https://stackoverflow.com/questions/52139192/github-pull-requests-template-not-showing * Sherlock 068.md: ERC721Pool's take will proceed with truncated collateral amount and full debt when borrower's collateral is fractional - Early revert take action when borrower's collateral is less than `1` if the pool is ERC721 - ensure collateral, quote tokens and bond calculations are alwyas performed for the number of NFTs that will be taken: instead passing Maths.min(borrower_.collateral, params_.takeCollateral) to _calculateTakeFlowsAndBondChange function calculate the rounded down collateral that can be taken from borrower Example: if borrower has 1.2 worth of colalteral and taker pass an amount of 2 collateral to take then calculations happens for Min(rounded(1.2), 2) = 1 NFT token If borrower debt can be covered with less than 1 NFT then _calculateTakeFlowsAndBondChange will return a fractioned NFT (0.5 for example) and the difference between 1 NFT taken and 0.5 will be paid with excess quote tokens --------- Co-authored-by: Ian Harvey Co-authored-by: Ian Harvey * fix sherlock issues 120 and 121 (#576) * Create standardized PR templates for contracts (#539) This PR creates three github PR templates that should be followed: * `logic_src_changes.md` * `cosmetic_src_changes.md` * `testing_or_misc_nonsrc_changes.md` To learn more about how GH templates work -> https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/creating-a-pull-request-template-for-your-repository Co-authored-by: Ian Harvey * Single template (#546) * merged all of the templates into one so the text will auto fill when a PR is created. * was running into this issue documented here with auto fill -> https://stackoverflow.com/questions/52139192/github-pull-requests-template-not-showing * fix sherlock issue 121 precision loss by multiplying before dividing * add _transferAjnaRewards method --------- Co-authored-by: Ian Harvey Co-authored-by: Ian Harvey Co-authored-by: Mike * Sherlock 163,140, 34 & 31: Remove support for non standard NFTs (#585) * merged all of the templates into one so the text will auto fill when a PR is created. * was running into this issue documented here with auto fill -> https://stackoverflow.com/questions/52139192/github-pull-requests-template-not-showing * Remove support for non standard NFTs (will have to be wrapped) * Sherlock 151: Changes for initial fix: (#596) * Sherlock 151: Changes for initial fix: - pass isManualEpoch_ param to _claimRewards function and do the epoch validation only if true (no need to validate for unstake which doesn't receive epoch as a param) - load stake struct from storage only once (and pass as a param to _claimRewards function) minimize calls to load ajna pool from stake storage * Changes after review: rename isManualEpoch to validateEpoch param * Sherlock 134.md changes after review: move check in LenderActions external library to keep logic in same place (#597) * Sherlock 98.md: (#577) * Create standardized PR templates for contracts (#539) This PR creates three github PR templates that should be followed: * `logic_src_changes.md` * `cosmetic_src_changes.md` * `testing_or_misc_nonsrc_changes.md` To learn more about how GH templates work -> https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/creating-a-pull-request-template-for-your-repository Co-authored-by: Ian Harvey * Single template (#546) * merged all of the templates into one so the text will auto fill when a PR is created. * was running into this issue documented here with auto fill -> https://stackoverflow.com/questions/52139192/github-pull-requests-template-not-showing * add reentrancy guard to PositionManager.mint(); update mint natspec --------- Co-authored-by: Ian Harvey Co-authored-by: Ian Harvey Co-authored-by: Mike * Sherlock 145: Take adjustments: `msg.sender` provides QT instead of `_callee` (#589) # Description of change ## High level * inside of `take` for ERC721 and ERC20 pools changed the argument passed in `_transferQuoteTokenFrom()` from `_callee` to `msg.sender`. * This update now makes the `_take` call match other implementations such as Maker * NOTE: 10-30K spike in gas cost because of change. # Description of bug or vulnerability and solution [ERC20Pool](https://github.com/sherlock-audit/2023-01-ajna/blob/main/contracts/src/ERC20Pool.sol#L403) and [ERC721Pool](https://github.com/sherlock-audit/2023-01-ajna/blob/main/contracts/src/ERC721Pool.sol#L405) implement the take functions, which buy collateral from auction in exchange for quote tokens. The address to pull quote tokens from is specified in the `_callee` argument, which allows anyone to call the functions and pass an address that has previously approved spending of the quote token to the pool. As a result, such an address will pay for the liquidation and will receive the collateral. The solution makes it so the `msg.sender` provides the QT when calling the `take()` method. # Contract size ## Pre Change ``` ============ Deployment Bytecode Sizes ============ ERC721Pool - 22,267B (90.60%) ERC20Pool - 20,972B (85.33%) ``` ## Post Change ``` ============ Deployment Bytecode Sizes ============ ERC721Pool - 22,267B (90.60%) ERC20Pool - 20,972B (85.33%) ``` # Gas usage ## Pre Change ``` | src/ERC20Pool.sol:ERC20Pool contract | | | | | | |--------------------------------------|-----------------|--------|--------|--------|---------| | take | 8550 | 158087 | 165977 | 520357 | 32 | src/ERC721Pool.sol:ERC721Pool contract | | | | | | |----------------------------------------|-----------------|--------|--------|--------|---------| | take | 11246 | 292029 | 375721 | 632601 | 11 | ``` ## Post Change ``` | src/ERC20Pool.sol:ERC20Pool contract | | | | | | |--------------------------------------|-----------------|--------|--------|--------|---------| | take | 8550 | 164634 | 184194 | 520356 | 33 | | src/ERC721Pool.sol:ERC721Pool contract | | | | | | |----------------------------------------|-----------------|--------|--------|----------|---------| | take | 11246 | 320810 | 387200 | 637400 | 12 | ``` * Sherlock 70: user can drawDebt that is below dust amount (#598) * ensure borrower debt exceeds dust amount regardless of loan count * Revert "ensure borrower debt exceeds dust amount regardless of loan count" This reverts commit c0a64f97c93e29b28533e9c1fc99fdf8f1b90cee. * re-applied changes lost when rebasing * Sherlock 68: (#599) - review update: add suggested revert to be excessively safe (for the case if current or future version of _calculateTakeFlowsAndBondChange() malfunctions producing collateral bigger than its collateral argument) * Sherlock 92: startClaimableReserveAuction using old inflator (#587) * Sherlock 92: startClaimableReserveAuction using old inflator * Add test to show interest accrued when start reserve auction * Revert "Sherlock 92: startClaimableReserveAuction using old inflator (#587)" This reverts commit 1414435c26f384ef13987bef1869b4b72326df46. * change safeTransferFrom to transferFrom to support Oasis (#592) * Create standardized PR templates for contracts (#539) This PR creates three github PR templates that should be followed: * `logic_src_changes.md` * `cosmetic_src_changes.md` * `testing_or_misc_nonsrc_changes.md` To learn more about how GH templates work -> https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/creating-a-pull-request-template-for-your-repository Co-authored-by: Ian Harvey * Single template (#546) * merged all of the templates into one so the text will auto fill when a PR is created. * was running into this issue documented here with auto fill -> https://stackoverflow.com/questions/52139192/github-pull-requests-template-not-showing * change safeTransferFrom to transferFrom --------- Co-authored-by: Ian Harvey Co-authored-by: Ian Harvey Co-authored-by: Mike * Sherlock 148: Use pool debt when calculating MOMP in Loans.update (#586) * Update comments: (#590) - ERC721.addCollateral changes bucketTokenIds - ERC721.repayDebt changes borrowerTokenIds and bucketTokenIds (when auction settled) - fix comment in Auctions._calculateTakeFlowsAndBondChange for take below NP * fix safeTransferFromWithPermit (#593) * Create standardized PR templates for contracts (#539) This PR creates three github PR templates that should be followed: * `logic_src_changes.md` * `cosmetic_src_changes.md` * `testing_or_misc_nonsrc_changes.md` To learn more about how GH templates work -> https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/creating-a-pull-request-template-for-your-repository Co-authored-by: Ian Harvey * Single template (#546) * merged all of the templates into one so the text will auto fill when a PR is created. * was running into this issue documented here with auto fill -> https://stackoverflow.com/questions/52139192/github-pull-requests-template-not-showing * fix safeTransferFromWithPermit --------- Co-authored-by: Ian Harvey Co-authored-by: Ian Harvey Co-authored-by: Mike * LPS and Exchange Rate as WADs - Sherlock 133 & 83 (#606) * LPs as WADs * - record lender LPs in remove collateral and quote token only if bucket doesn't go bankrupt - for remove colalteral use the same way to calculate LPs as when rewarding, to avoid precision issues - tearDown should consider that bucket can go bankrupt, hence lender LPs should be checked = 0 taking this into account (lenderInfo function is doing this) - all tests passing but testDepositTwoActorSameBucket fails in tearDown - that's because there are quote tokens remaining without being backed by LPs (were previously redeemed in tearDown for collateral) * Use consistent way to calculate LPs reported to scaled amounts, remove unscaled rate exchange, fix tests Couple of take tests fail in tearDown leaving small dust deposit * Cleanup Maths library, update comments * Fix remaining tests * Fix sequence to empty buckets, one single settle test remaining failing in tearDown * Add bucket clean out sequence, make consistent for removeCollateral too (per Sherlock 133) * Add bankruptcy check for move quote token from bucket and unit test * Guard against underflows in removeQuoteToken (due to roundings when transforming from scaled to unscaled amount) * Changes after review: - calculate amount using collateralAmount_ rather than bucketCollateral - remove dead code in remove quote token * Sherlock 39: expiration timestamp and slippage control (#600) * ignore forge script tx broadcasts * provide LUP limit when pulling collateral * expiration checks for adding to buckets * unit tests for addQuote and addCollateral expiration * expiration checks for moveQuote * ensure borrower debt exceeds dust amount regardless of loan count * Revert "ensure borrower debt exceeds dust amount regardless of loan count" This reverts commit c0a64f97c93e29b28533e9c1fc99fdf8f1b90cee. * feedback from PR #600 (#604) * removed comment * Address review comments, update comment for _revertIfLupDroppedBelowL… (#607) * Address review comments, update comment for _revertIfLupDroppedBelowLimit, changed LimitIndexReached to LimitIndexExceeded * Remove lup Id variable, calculate LUP price directly * Remove unused struct member --------- Co-authored-by: grandizzy <38490174+grandizzy@users.noreply.github.com> * Fix test compile * Protocol invariants testing (#609) * Invariants * Exclude invariants from code coverage and GH action * Fix coverage GH target * Changes after review * Disallow auctioned borrowers to draw more debt or pull collateral if auction is not settled. (#611) This can happen in a scenario where user becomes recollateralized to the new LUP. Without these checks, and since borrow and pull doesn't settle auctions, borrower is not removed from auction queue but added in loan queue as well, which violate invariant - **A3**: number of borrowers with debt = number of loans + number of auctioned borrowers * Sherlock 73 (#591) * Create standardized PR templates for contracts (#539) This PR creates three github PR templates that should be followed: * `logic_src_changes.md` * `cosmetic_src_changes.md` * `testing_or_misc_nonsrc_changes.md` To learn more about how GH templates work -> https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/creating-a-pull-request-template-for-your-repository Co-authored-by: Ian Harvey * Single template (#546) * merged all of the templates into one so the text will auto fill when a PR is created. * was running into this issue documented here with auto fill -> https://stackoverflow.com/questions/52139192/github-pull-requests-template-not-showing * update getNFTSubsetHash to uniquely sort array of tokenIds * add natspec and slight gas optimization * change encodePacked to encode to add byte padding * pool deployer gas optimization * check sort order instead of sorting on chain * pr feedback to check for duplicate tokenIds --------- Co-authored-by: Ian Harvey Co-authored-by: Ian Harvey Co-authored-by: Mike --------- Co-authored-by: Ed Noepel <46749157+EdNoepel@users.noreply.github.com> Co-authored-by: mattcushman <36414299+mattcushman@users.noreply.github.com> Co-authored-by: mwc Co-authored-by: Ed Noepel Co-authored-by: Prateek Gupta Co-authored-by: prateek105 Co-authored-by: Ian Harvey Co-authored-by: Ian Harvey Co-authored-by: Mike Hathaway Co-authored-by: Mike --- .github/workflows/code-coverage.yml | 2 +- .github/workflows/forge-tests.yml | 55 - .github/workflows/size-check.yml | 17 +- .gitignore | 3 +- Makefile | 13 +- README.md | 60 +- check-code-coverage.sh | 2 +- deploy.sh | 63 - deploy.sol | 36 + docs/Functions.md | 4 +- docs/drawio/addCollateral.drawio | 1 + docs/drawio/addQuoteToken.drawio | 1 + docs/drawio/bucketTake.drawio | 1 + docs/drawio/components.drawio | 1 + docs/drawio/drawDebt.drawio | 1 + docs/drawio/kick.drawio | 1 + docs/drawio/kickWithDeposit.drawio | 1 + docs/drawio/moveQuoteToken.drawio | 1 + docs/drawio/poolContractsArchitecture.drawio | 1 + docs/drawio/removeCollateral.drawio | 1 + docs/drawio/removeQuoteToken.drawio | 1 + docs/drawio/repayDebt.drawio | 1 + docs/drawio/settle.drawio | 1 + docs/drawio/take.drawio | 1 + docs/html/addCollateral.html | 11 + docs/html/addQuoteToken.html | 11 + docs/html/bucketTake.html | 11 + docs/html/components.html | 11 + docs/html/drawDebt.html | 11 + docs/html/kick.html | 11 + docs/html/kickWithDeposit.html | 11 + docs/html/moveQuoteToken.html | 11 + docs/html/poolContractsArchitecture.html | 11 + docs/html/removeCollateral.html | 11 + docs/html/removeQuoteToken.html | 11 + docs/html/repayDebt.html | 11 + docs/html/settle.html | 11 + docs/html/take.html | 11 + docs/jpeg/ajnaContractsArchitecture.jpeg | Bin 0 -> 83293 bytes docs/jpeg/poolContract.jpeg | Bin 0 -> 51883 bytes docs/jpeg/poolContractsArchitecture.jpeg | Bin 0 -> 81800 bytes foundry.toml | 6 + src/ERC20Pool.sol | 112 +- src/ERC20PoolFactory.sol | 8 +- src/ERC721Pool.sol | 123 +- src/ERC721PoolFactory.sol | 69 +- src/PoolInfoUtils.sol | 35 +- src/PositionManager.sol | 11 +- src/RewardsManager.sol | 112 +- src/base/FlashloanablePool.sol | 56 +- src/base/PermitERC721.sol | 5 +- src/base/Pool.sol | 72 +- src/base/PoolDeployer.sol | 5 +- src/interfaces/pool/IPool.sol | 2 +- src/interfaces/pool/commons/IPoolErrors.sol | 17 +- src/interfaces/pool/commons/IPoolEvents.sol | 6 +- .../pool/commons/IPoolInternals.sol | 51 +- .../pool/commons/IPoolLenderActions.sol | 8 +- .../pool/commons/IPoolLiquidationActions.sol | 3 +- .../commons/IPoolReserveAuctionActions.sol | 2 +- src/interfaces/pool/commons/IPoolState.sol | 25 +- .../pool/erc20/IERC20PoolBorrowerActions.sol | 6 +- .../pool/erc20/IERC20PoolLenderActions.sol | 9 +- .../pool/erc721/IERC721NonStandard.sol | 20 - .../erc721/IERC721PoolBorrowerActions.sol | 6 +- .../pool/erc721/IERC721PoolFactory.sol | 9 + .../pool/erc721/IERC721PoolLenderActions.sol | 5 +- .../position/IPositionManagerDerivedState.sol | 12 +- .../position/IPositionManagerOwnerActions.sol | 8 +- .../rewards/IRewardsManagerErrors.sol | 5 + .../rewards/IRewardsManagerState.sol | 16 +- src/libraries/external/Auctions.sol | 360 +- src/libraries/external/BorrowerActions.sol | 163 +- src/libraries/external/LenderActions.sol | 236 +- src/libraries/helpers/PoolHelper.sol | 4 +- src/libraries/helpers/RevertsHelper.sol | 41 +- src/libraries/internal/Buckets.sol | 33 +- src/libraries/internal/Loans.sol | 20 +- src/libraries/internal/Maths.sol | 29 - tests/INVARIANTS.md | 69 + tests/README.md | 4 + tests/brownie/test_scaled_pool.py | 19 +- tests/brownie/test_stable_volatile.py | 14 +- tests/forge/ERC20Pool/ERC20DSTestPlus.sol | 83 +- .../ERC20Pool/ERC20FlashloanCollateral.t.sol | 141 - tests/forge/ERC20Pool/ERC20PoolBorrow.t.sol | 1003 +++-- .../forge/ERC20Pool/ERC20PoolCollateral.t.sol | 727 ++-- tests/forge/ERC20Pool/ERC20PoolFactory.t.sol | 88 +- .../forge/ERC20Pool/ERC20PoolFlashloan.t.sol | 266 ++ .../ERC20Pool/ERC20PoolGasLoadTest.t.sol | 173 +- .../forge/ERC20Pool/ERC20PoolInfoUtils.t.sol | 106 +- .../ERC20PoolInterestRateAndEMAs.t.sol | 566 ++- .../ERC20PoolLiquidationsArbTake.t.sol | 1145 +++--- .../ERC20PoolLiquidationsDepositTake.t.sol | 1130 +++--- .../ERC20Pool/ERC20PoolLiquidationsKick.t.sol | 736 ++-- ...ERC20PoolLiquidationsKickWithDeposit.t.sol | 1003 +++-- .../ERC20Pool/ERC20PoolLiquidationsMisc.t.sol | 674 ++-- .../ERC20PoolLiquidationsScaled.t.sol | 11 +- .../ERC20PoolLiquidationsSettle.t.sol | 1213 +++--- .../ERC20Pool/ERC20PoolLiquidationsTake.t.sol | 2292 ++++++------ tests/forge/ERC20Pool/ERC20PoolLoanHeap.t.sol | 186 + .../forge/ERC20Pool/ERC20PoolMulticall.t.sol | 124 +- .../forge/ERC20Pool/ERC20PoolPrecision.t.sol | 579 ++- .../ERC20Pool/ERC20PoolPurchaseQuote.t.sol | 477 ++- .../forge/ERC20Pool/ERC20PoolQuoteToken.t.sol | 1829 +++++----- .../ERC20Pool/ERC20PoolReserveAuction.t.sol | 115 + .../ERC20Pool/ERC20PoolTransferLPTokens.t.sol | 632 ---- .../ERC20Pool/ERC20PoolTransferLPs.t.sol | 541 +++ .../ERC20Pool/ERC20SafeTransferTokens.sol | 4 +- .../invariants/BasicInvariants.t.sol | 248 ++ .../ERC20Pool/invariants/InvariantTest.sol | 76 + .../invariants/LiquidationInvariant.t.sol | 152 + .../invariants/ReserveInvariants.t.sol | 59 + tests/forge/ERC20Pool/invariants/TestBase.sol | 38 + .../invariants/handlers/BaseHandler.sol | 153 + .../invariants/handlers/BasicPoolHandler.sol | 359 ++ .../invariants/handlers/IBaseHandler.sol | 22 + .../handlers/LiquidationPoolHandler.sol | 148 + .../handlers/ReservePoolHandler.sol | 48 + .../regression/RegressionTestBasic.t.sol | 121 + .../RegressionTestLiquidation.t.sol | 70 + .../regression/RegressionTestReserves.t.sol | 50 + tests/forge/ERC721Pool/ERC721DSTestPlus.sol | 154 +- .../ERC721NonStandardTokenTransfer.sol | 121 - tests/forge/ERC721Pool/ERC721PoolBorrow.t.sol | 673 ++-- .../ERC721Pool/ERC721PoolCollateral.t.sol | 1290 ++++--- .../forge/ERC721Pool/ERC721PoolFactory.t.sol | 142 +- ...nQuote.t.sol => ERC721PoolFlashloan.t.sol} | 40 +- .../forge/ERC721Pool/ERC721PoolInterest.t.sol | 785 ++-- .../ERC721PoolLiquidationsDepositTake.t.sol | 308 ++ .../ERC721PoolLiquidationsKick.t.sol | 370 +- .../ERC721PoolLiquidationsSettle.t.sol | 441 ++- .../ERC721PoolLiquidationsSettleAuction.t.sol | 1288 +++++++ .../ERC721PoolLiquidationsTake.t.sol | 710 ++-- .../ERC721Pool/ERC721PoolPurchaseQuote.t.sol | 447 +-- .../ERC721Pool/ERC721PoolReserveAuction.t.sol | 129 +- tests/forge/FenwickTree.t.sol | 21 +- tests/forge/Heap.t.sol | 4 +- tests/forge/MathTest.t.sol | 26 - tests/forge/PositionManager.t.sol | 3245 ++++++++--------- tests/forge/RewardsManager.t.sol | 156 +- .../interactions/BalancerUniswapExample.sol | 4 +- .../ERC20TakeWithExternalLiquidity.t.sol | 33 +- .../ERC721TakeWithExternalLiquidity.sol | 43 +- tests/forge/interactions/Interfaces.sol | 3 +- tests/forge/interactions/NFTTakeExample.sol | 24 + .../PurchaseQuoteWithExternalLiquidity.t.sol | 2 +- tests/forge/utils/DSTestPlus.sol | 71 +- tests/forge/utils/FenwickTreeInstance.sol | 71 +- tests/forge/utils/FlashloanBorrower.sol | 30 +- tests/forge/utils/HeapInstance.sol | 12 +- 151 files changed, 16485 insertions(+), 13785 deletions(-) delete mode 100644 .github/workflows/forge-tests.yml delete mode 100755 deploy.sh create mode 100644 deploy.sol create mode 100644 docs/drawio/addCollateral.drawio create mode 100644 docs/drawio/addQuoteToken.drawio create mode 100644 docs/drawio/bucketTake.drawio create mode 100644 docs/drawio/components.drawio create mode 100644 docs/drawio/drawDebt.drawio create mode 100644 docs/drawio/kick.drawio create mode 100644 docs/drawio/kickWithDeposit.drawio create mode 100644 docs/drawio/moveQuoteToken.drawio create mode 100644 docs/drawio/poolContractsArchitecture.drawio create mode 100644 docs/drawio/removeCollateral.drawio create mode 100644 docs/drawio/removeQuoteToken.drawio create mode 100644 docs/drawio/repayDebt.drawio create mode 100644 docs/drawio/settle.drawio create mode 100644 docs/drawio/take.drawio create mode 100644 docs/html/addCollateral.html create mode 100644 docs/html/addQuoteToken.html create mode 100644 docs/html/bucketTake.html create mode 100644 docs/html/components.html create mode 100644 docs/html/drawDebt.html create mode 100644 docs/html/kick.html create mode 100644 docs/html/kickWithDeposit.html create mode 100644 docs/html/moveQuoteToken.html create mode 100644 docs/html/poolContractsArchitecture.html create mode 100644 docs/html/removeCollateral.html create mode 100644 docs/html/removeQuoteToken.html create mode 100644 docs/html/repayDebt.html create mode 100644 docs/html/settle.html create mode 100644 docs/html/take.html create mode 100644 docs/jpeg/ajnaContractsArchitecture.jpeg create mode 100644 docs/jpeg/poolContract.jpeg create mode 100644 docs/jpeg/poolContractsArchitecture.jpeg delete mode 100644 src/interfaces/pool/erc721/IERC721NonStandard.sol create mode 100644 tests/INVARIANTS.md delete mode 100644 tests/forge/ERC20Pool/ERC20FlashloanCollateral.t.sol create mode 100644 tests/forge/ERC20Pool/ERC20PoolFlashloan.t.sol create mode 100644 tests/forge/ERC20Pool/ERC20PoolLoanHeap.t.sol create mode 100644 tests/forge/ERC20Pool/ERC20PoolReserveAuction.t.sol delete mode 100644 tests/forge/ERC20Pool/ERC20PoolTransferLPTokens.t.sol create mode 100644 tests/forge/ERC20Pool/ERC20PoolTransferLPs.t.sol create mode 100644 tests/forge/ERC20Pool/invariants/BasicInvariants.t.sol create mode 100644 tests/forge/ERC20Pool/invariants/InvariantTest.sol create mode 100644 tests/forge/ERC20Pool/invariants/LiquidationInvariant.t.sol create mode 100644 tests/forge/ERC20Pool/invariants/ReserveInvariants.t.sol create mode 100644 tests/forge/ERC20Pool/invariants/TestBase.sol create mode 100644 tests/forge/ERC20Pool/invariants/handlers/BaseHandler.sol create mode 100644 tests/forge/ERC20Pool/invariants/handlers/BasicPoolHandler.sol create mode 100644 tests/forge/ERC20Pool/invariants/handlers/IBaseHandler.sol create mode 100644 tests/forge/ERC20Pool/invariants/handlers/LiquidationPoolHandler.sol create mode 100644 tests/forge/ERC20Pool/invariants/handlers/ReservePoolHandler.sol create mode 100644 tests/forge/ERC20Pool/regression/RegressionTestBasic.t.sol create mode 100644 tests/forge/ERC20Pool/regression/RegressionTestLiquidation.t.sol create mode 100644 tests/forge/ERC20Pool/regression/RegressionTestReserves.t.sol delete mode 100644 tests/forge/ERC721Pool/ERC721NonStandardTokenTransfer.sol rename tests/forge/ERC721Pool/{ERC721FlashloanQuote.t.sol => ERC721PoolFlashloan.t.sol} (89%) create mode 100644 tests/forge/ERC721Pool/ERC721PoolLiquidationsDepositTake.t.sol create mode 100644 tests/forge/ERC721Pool/ERC721PoolLiquidationsSettleAuction.t.sol 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/.github/workflows/forge-tests.yml b/.github/workflows/forge-tests.yml deleted file mode 100644 index b77869bef..000000000 --- a/.github/workflows/forge-tests.yml +++ /dev/null @@ -1,55 +0,0 @@ -name: Forge Tests - -on: [push] - -env: - ## Loads environment from secrets - ETH_RPC_URL: ${{secrets.ETH_RPC_URL}} - -jobs: - tests: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - with: - submodules: recursive - - - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1 - with: - version: nightly - - - name: Create ids - shell: bash - run: | - echo "##[set-output name=dir;]$(echo ${GITHUB_REF#refs/heads/}-gas-reports)" - echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})" - id: gas - - - name: Cache gas snasphot - id: cache-gas - uses: actions/cache@v3 - with: - path: gas-reports-* - key: ${{ steps.gas.outputs.dir }}-${{ github.run_id }} - restore-keys: | - ${{ steps.gas.outputs.dir }}- - - - name: Run tests and compare gas - shell: bash - run: | - prevRuns=$(find . -name "gas-reports-*" -printf '.' | wc -m) - if [ $prevRuns = "0" ]; then echo "No previous run"; else dir=$(ls -td -- gas-reports-* | head -n 1) && forge snapshot --no-match-test testLoad --diff $dir/.gas-snapshot;fi 2>&1 | tee gas-diff-output.txt - mkdir -p gas-reports-${{ github.run_id }} - forge snapshot --no-match-test testLoad --gas-report --snap gas-reports-${{ github.run_id }}/.gas-snapshot - echo "##[set-output name=gas-diff;]$(echo $(cat gas-diff-output.txt|tail -1|sed -e 's/\x1b\[[0-9;]*m//g'))" - id: forge-tests - - - name: Send to Discord - uses: appleboy/discord-action@master - with: - webhook_id: ${{ secrets.DISCORD_ID }} - webhook_token: ${{ secrets.DISCORD_TOKEN }} - message: https://github.com/ajna-finance/contracts/commit/${{ github.sha }} ```${{ steps.forge-tests.outputs.gas-diff }} compared with last run in branch ${{ steps.gas.outputs.branch }}``` - - timeout-minutes: 10 \ No newline at end of file diff --git a/.github/workflows/size-check.yml b/.github/workflows/size-check.yml index de68bb5a8..10f623fcd 100644 --- a/.github/workflows/size-check.yml +++ b/.github/workflows/size-check.yml @@ -9,7 +9,8 @@ jobs: submodules: recursive - name: Cache compiler installations - uses: actions/cache@v2 + id: cache-compiler + uses: actions/cache@v3 with: path: | ~/.solcx @@ -32,13 +33,13 @@ jobs: - name: Set pip cache directory path id: pip-cache-dir-path run: | - echo "::set-output name=dir::$(pip cache dir)" + echo "PIP_CACHE_DIR=$(pip cache dir)" >> $GITHUB_ENV - name: Restore pip cache - uses: actions/cache@v2 + uses: actions/cache@v3 id: pip-cache with: path: | - ${{ steps.pip-cache-dir-path.outputs.dir }} + ${PIP_CACHE_DIR} key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements-dev.txt') }} restore-keys: | ${{ runner.os }}-pip-${{ hashFiles('**/requirements-dev.txt') }} @@ -47,11 +48,11 @@ jobs: run: pip install -r requirements-dev.txt - name: Run check-size script + id: check-size run: | mkdir -p size-reports-${{ github.run_id }} ./check-size.sh | tee size-reports-${{github.run_id}}/size-report - echo "##[set-output name=size-report;]$(echo $(cat size-reports-${{github.run_id}}/size-report|tail -1))" - id: check-size + echo "SIZE_REPORT=$(echo $(cat size-reports-${{github.run_id}}/size-report|tail -1))" >> $GITHUB_OUTPUT - name: Send size report to Discord uses: appleboy/discord-action@master @@ -59,5 +60,5 @@ jobs: webhook_id: ${{ secrets.DISCORD_ID }} webhook_token: ${{ secrets.DISCORD_TOKEN }} username: "Contract Size Reporter" - message: Largest contract size for `${{ github.ref }}` ```${{ steps.check-size.outputs.size-report }}``` - timeout-minutes: 2 \ No newline at end of file + message: Largest contract size for `${{ github.ref }}` ```${{ steps.check-size.outputs.SIZE_REPORT }}``` + timeout-minutes: 3 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 5e9c34d5d..1357170a3 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,5 @@ reports/ coverage/ *.info report/ -keystore/ \ No newline at end of file +keystore/ +broadcast/ \ No newline at end of file diff --git a/Makefile b/Makefile index 80d2ddfff..5a0c59780 100644 --- a/Makefile +++ b/Makefile @@ -14,13 +14,20 @@ 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 analyze: slither src/. ; slither src/libraries/external/. + + +# Deployment +deploy-contracts: + forge script ./deploy.sol \ + --rpc-url ${ETH_RPC_URL} --sender ${DEPLOY_ADDRESS} --keystore ${DEPLOY_KEY} --broadcast -vvv diff --git a/README.md b/README.md index 834b02766..22eeddcb6 100644 --- a/README.md +++ b/README.md @@ -2,16 +2,21 @@ The Ajna protocol is a non-custodial, peer-to-peer, permissionless lending, borrowing and trading system that requires no governance or external price feeds to function. The protocol consists of pools: pairings of quote tokens provided by lenders and collateral tokens provided by borrowers. Ajna is capable of accepting fungible tokens as quote tokens and both fungible and non-fungible tokens as collateral tokens. -## Limitations -- The following types of tokens are incompatible with Ajna, and no countermeasures exist to explicitly prevent creating a pool with such tokens: - - Fungible tokens whose balance rebases. - - NFTs which charge a fee on transfer. - - Fungible tokens with more than 18 decimals or 0 decimals. +## Accepted tokens: +- Fungible tokens (following the [ERC20 token standard](https://eips.ethereum.org/EIPS/eip-20)). +- Non-fungible tokens (following the [ERC721 token standard](https://eips.ethereum.org/EIPS/eip-721)) - Special considerations have been made to support specific NFTs with nonstandard ERC721 implementations, including _CryptoPunks_ and _CryptoKitties_. This support is limited to Ethereum mainnet. + +### Token limitations +- The following types of tokens are incompatible with Ajna, and no countermeasures exist to explicitly prevent creating a pool with such tokens, actors should use them at their own risk: + - NFT and fungible tokens which charge a fee on transfer. + - Fungible tokens whose balance rebases. + - Fungible tokens with more than 18 decimals or 0 decimals. - Borrowers cannot draw debt from a pool in the same block as when the pool was created. - With the exception of quantized prices, pool inputs and most accumulators are not explicitly limited. The pool will stop functioning when the bounds of a `uint256` need to be exceeded to process a request. + ## Development ### Requirements - `python` 3.0+ @@ -150,35 +155,38 @@ Modifications to, or notices of actions by Licensor, contemplated above or under ## Deployment -A deployment script has been created to automate deployment of libraries and factory contracts. -To use it, set up an environment with the following: -- **AJNA_TOKEN** - address of the AJNA token on your target chain -- **ETH_RPC_URL** - node pointing to the target chain -- **DEPLOY_KEY** - path to the JSON keystore file for your deployment account +A deployment script has been created to automate deployment of libraries, factory contracts, and manager contracts. + +To use it, ensure the following env variables are in your `.env` file or exported into your environment. +| Environment Variable | Purpose | +|----------------------|---------| +| `AJNA_TOKEN` | address of the AJNA token on your target chain +| `DEPLOY_ADDRESS` | address from which you wish to deploy +| `DEPLOY_KEY` | path to the JSON keystore file for the deployment address +| `ETHERSCAN_API_KEY` | required to verify contracts +| `ETH_RPC_URL` | node on your target deployment network -Ensure your deployment account is funded with some ETH for gas. -The deployment script takes no arguments, and interactively prompts for your keystore password: +Since contract source has not yet been made public, the `--verify` switch has been omitted. To run: + ``` -./deploy.sh +make deploy-contracts ``` Upon completion, contract addresses will be printed to `stdout`: ``` -Deploying to chain with AJNA token address 0xDD576260ed60AaAb798D8ECa9bdBf33D70E077F4 -Enter keystore password: -Deploying libraries... -Deployed Auctions to 0xDD576260ed60AaAb798D8ECa9bdBf33D70E077F4 -Deployed LenderActions to 0x4c08A2ec1f5C067DC53A5fCc36C649501F403b93 -Deployed PoolCommons to 0x8BBCA51044d00dbf16aaB8Fd6cbC5B45503B898b -Deploying factories... -Deployed ERC20PoolFactory to 0xED625fbf62695A13d2cADEdd954b23cc97249988 -Deployed ERC721PoolFactory to 0x775D30918A42160bC7aE56BA1660E32ff50CF6dC -Deploying PoolInfoUtils... -Deployed PoolInfoUtils to 0xd8A51cE16c7665111401C0Ba2ABEcE03B847b4e6 +== Logs == + Deploying to chain with AJNA token address 0x34A1D3fff3958843C43aD80F30b94c510645C... + === Deployment addresses === + ERC20 factory 0x50EEf481cae4250d252Ae577A09bF514f224C... + ERC721 factory 0x62c20Aa1e0272312BC100b4e23B4DC1Ed96dD... + PoolInfoUtils 0xDEb1E9a6Be7Baf84208BB6E10aC9F9bbE1D70... + PositionManager 0xD718d5A27a29FF1cD22403426084bA0d47986... + RewardsManager 0x4f559F30f5eB88D635FDe1548C4267DB8FaB0... + ``` -Record the factory addresses. +Record these addresses. ### Validation @@ -203,4 +211,4 @@ cast send ${DAI_TOKEN} "approve(address,uint256)" ${WBTC_DAI_POOL} 50000ether \ --from ${DEPLOY_ADDRESS} --keystore ${DEPLOY_KEY} cast send ${WBTC_DAI_POOL} "addQuoteToken(uint256,uint256)" 100ether 3232 \ --from ${DEPLOY_ADDRESS} --keystore ${DEPLOY_KEY} -``` +``` \ No newline at end of file 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/deploy.sh b/deploy.sh deleted file mode 100755 index c23e79d98..000000000 --- a/deploy.sh +++ /dev/null @@ -1,63 +0,0 @@ -#!/bin/bash - -# load environment variables from .env file -set -o allexport -source .env -set +o allexport - -echo Deploying to chain with AJNA token address ${AJNA_TOKEN:?} - -read -p "Enter keystore password: " -s password - -regex="Deployed to: ([0-9xa-fA-F]+)" -linkage=() - -echo -echo Deploying libraries... -libraries=( Auctions LenderActions BorrowerActions PoolCommons PositionNFTSVG ) -for contract in "${libraries[@]}" -do - createlib="forge create --rpc-url ${ETH_RPC_URL:?} --keystore ${DEPLOY_KEY:?} --password ${password:?} \ - src/libraries/external/$contract.sol:$contract" - output=$($createlib) - if [[ $output =~ $regex ]] - then - address=${BASH_REMATCH[1]} - printf "Deployed %20s to %s\n" ${contract:0:20} $address - linkage+="--libraries src/libraries/external/$contract.sol:$contract:$address " - else - echo $contract was not deployed: $output - exit 1 - fi -done - -echo Deploying factories... -factories=( ERC20PoolFactory ERC721PoolFactory ) -for contract in "${factories[@]}" -do - createfactory="forge create --rpc-url ${ETH_RPC_URL:?} --keystore ${DEPLOY_KEY:?} --password ${password:?} \ - src/$contract.sol:$contract --constructor-args ${AJNA_TOKEN:?} ${linkage}" - output=$($createfactory) - if [[ $output =~ $regex ]] - then - address=${BASH_REMATCH[1]} - printf "Deployed %20s to %s\n" ${contract:0:20} $address - else - echo $contract was not deployed: $output - exit 2 - fi -done - -echo Deploying PoolInfoUtils... -contract=PoolInfoUtils -create="forge create --rpc-url ${ETH_RPC_URL:?} --keystore ${DEPLOY_KEY:?} --password ${password:?} \ - src/$contract.sol:$contract ${linkage}" -output=$($create) -if [[ $output =~ $regex ]] -then - address=${BASH_REMATCH[1]} - printf "Deployed %20s to %s\n" ${contract:0:20} $address -else - echo $contract was not deployed: $output - exit 1 -fi \ No newline at end of file diff --git a/deploy.sol b/deploy.sol new file mode 100644 index 000000000..3b50b91f6 --- /dev/null +++ b/deploy.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.14; + +import { Script } from "forge-std/Script.sol"; +import "forge-std/console.sol"; + +import { ERC20PoolFactory } from 'src/ERC20PoolFactory.sol'; +import { ERC721PoolFactory } from 'src/ERC721PoolFactory.sol'; +import { PoolInfoUtils } from 'src/PoolInfoUtils.sol'; +import { PositionManager } from 'src/PositionManager.sol'; +import { RewardsManager } from 'src/RewardsManager.sol'; + +contract Deploy is Script { + address ajna; + + function run() public { + ajna = vm.envAddress("AJNA_TOKEN"); + console.log("Deploying to chain with AJNA token address %s", ajna); + + vm.startBroadcast(); + ERC20PoolFactory erc20factory = new ERC20PoolFactory(ajna); + ERC721PoolFactory erc721factory = new ERC721PoolFactory(ajna); + PoolInfoUtils poolInfoUtils = new PoolInfoUtils(); + + PositionManager positionManager = new PositionManager(erc20factory, erc721factory); + RewardsManager rewardsManager = new RewardsManager(ajna, positionManager); + vm.stopBroadcast(); + + console.log("=== Deployment addresses ==="); + console.log("ERC20 factory %s", address(erc20factory)); + console.log("ERC721 factory %s", address(erc721factory)); + console.log("PoolInfoUtils %s", address(poolInfoUtils)); + console.log("PositionManager %s", address(positionManager)); + console.log("RewardsManager %s", address(rewardsManager)); + } +} \ No newline at end of file diff --git a/docs/Functions.md b/docs/Functions.md index ac1d3b0c8..7e12e88ea 100644 --- a/docs/Functions.md +++ b/docs/Functions.md @@ -129,7 +129,7 @@ emit events: - LenderActions.transferLPs(): - - TransferLPTokens + - TransferLPs ### kick external libraries call: @@ -370,7 +370,7 @@ - BorrowerActions.drawDebt() - borrower not sender BorrowerNotSender() - borrower debt less than pool min debt AmountLTMinDebt() - - limit price reached LimitIndexReached() + - limit price reached LimitIndexExceeded() - borrower cannot draw more debt BorrowerUnderCollateralized() emit events: diff --git a/docs/drawio/addCollateral.drawio b/docs/drawio/addCollateral.drawio new file mode 100644 index 000000000..b62d8a395 --- /dev/null +++ b/docs/drawio/addCollateral.drawio @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/docs/drawio/addQuoteToken.drawio b/docs/drawio/addQuoteToken.drawio new file mode 100644 index 000000000..0ea7fef45 --- /dev/null +++ b/docs/drawio/addQuoteToken.drawio @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/docs/drawio/bucketTake.drawio b/docs/drawio/bucketTake.drawio new file mode 100644 index 000000000..942266631 --- /dev/null +++ b/docs/drawio/bucketTake.drawio @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/docs/drawio/components.drawio b/docs/drawio/components.drawio new file mode 100644 index 000000000..ddebbac67 --- /dev/null +++ b/docs/drawio/components.drawio @@ -0,0 +1 @@ +7V1bc6M4Fv41rtp9SBd3zGPuM1vp3mzSszP9tCWDbLORkVfg2JlfPxJINiDsYEcCsu2ngAxH6NO5fOdIkJF9vdjcE7Ccf8URRCPLiDYj+2ZkWaZjO/QPa3krWnzDLxpmJI74RbuG5/hPyBsN3rqKI5hWLswwRlm8rDaGOElgmFXaACF4Xb1silG11yWYQanhOQRIbv09jrJ50Tq2/F37LzCezUXPphcUvyyAuJiPJJ2DCK9LTfbtyL4mGGfF0WJzDREDT+BS3He359ftgxGYZG1usJZ//rj9/o/pb/Nvz5N79+bmP6/mBZfyCtCKD/hyFWYxTlL+0NmbQCJdxwsEEnp2tZ7HGXxegpD9tKYTT9vm2QLRM5MeTmOErjHCJL/RHk+nBnBou/zEontIMrgpNfER3EO8gBl5o5eIX8ducQtXJ9fn6K53k2MKxOeVieGNgCvEbCt7hxk94LAdAaEnQXgIujQDJONqbqjBxAvsKiaOjIkdBDImtjZM/AZMPJQx3cB0nGVwvP+tsPjhIs2BuaQXUEw2OTzid3o0Y39/TTJIEsAeAcUTAkhM3QMXPiHiKtFCn77oUTTXZoZCnFWVF6B4ltDjELKOaAObiJg6hEv+wyKOInb7FYH0ccEEialc4jjJciTdq5F7w2StMlwMKRedZgS/QGEZCc4VomwsvEnWiYOK1954zAZj8RuMxdGlF04gKcYj9ea/QLSkWCvzONPpBHpTrlTc2Mxma1ODrBX4FRO0ZaD9BgP0PU04jyWYr1bhC8wUevUtxgo82JYUDMery4p6A5fUmIcK4VjY7HAgFIJLGH6lpGiwAJoVAH3D6R1AmZ09YKCSmqk14vHgAAwkqGBE6T0/xSSb4xmmZOJ213pF8CqJYMSD+u6aB4yXHMX/wix743GFxfgaxrugYzERMIkuWSayC/C05S5mQzlMAVO8IiE8ND5ONSmnnMEWAY0N/uBUEohAFr9Wcx/102JJen1IpTVQZtOvGvsAKLNpN4CiiDPfbs6cuaZ6n4g0y9ZCvRL1J5Bcdpiqq4LTsb5UU/gGrtxxmHDdvaYXxa+sR67GO8tCcJrJdnZBr40IWN/ASfa3v5fsKRdTtbLjJRO4BG9V0Y3GW+mste3m/fZhufUcDYEJRFcgfJnlkfg0Oz9eL+tqKWJSSS3HnabKMvejtOFT2rzlBYOzeTnaskLENV4sPhm4blBB1vR6h9b0Jfw6YN1wE2d/8NvZ8Q92TNWuOLvZlH66eRMnCR3vH7sL2ekPIY+d7G7Lz8R9Bxh+iECaxqFKks9N512SL3RnKCx/3KcamCeqgWVWFOGLYXjvKEN+9ghJTGFj5Pc9DVGdA7ZVj2HlgJZcHDo23TH3pTurFLLuKwsFH8h3GGIPjJToS3rq6qKEAwkDlGLJdrWUP9OovCDZFGMujC+Gb1XijAigrbWFC39kYy9JtnynJlcUyoUQPJ2mVLHrCrd9yg94KJn3MxKgLvqHxmQ6CdWULJzAqwAVBHKgt1xHDvTbBWj1Nry/ZHFcdgMmNFkAYVZoJj9UlzxdI2Y3CgU+Qeb0QRK+3a8AiVSK/rpCuRNBKoW2TxdPzU1BFP2LtsLvNOVL1Ke+YLkk+BU+PP5znUCSzuOl+j4WtAOdYyBQdw9MJdMpJA+PqXrhL3H4okfq73E254tr6jtYU+GsJHOFk0gDKnlt+hqBeMFi6RNMIXmFfAuNhgkGL6KP9P+7CNSW4rjHhlGvWvi3AjlbbirumoG2ICov8ikjwhFEcAYyxoZZSEmZCmGWA2x+Hmq8j2gdTYIds8pUTeOQTjTT3eoKqeDQ+pmuKGaes61BqdR2G2krlfqYm+m3KLetwPwYlQowuopyZq3kMkE4fBmVay7moYl9t+ZiOS1rLiI+DaXoIi8q3CGQzhEGCdPZ4Wa/rldfnzEtdyzH7m4r3ZaqhUPNCTDjjlM20Wz7kHpamou+g1BDTgY2d/Jz60ljz3R65x/aL5tWdx0KAWWTNBpM0te2rOf0Eua2IascsErxS1XIiqgt5A96anD9QMwbt4x5QgfUxbxmLu0aUkiw3ZqUYlT8Rg20Wt5nfft0bRlq42gQhOb0IGE5ooosbb8xHdvrO47abq9Wa3ZptadboNg00YMFfmx25aU+aiW+ZQ7XTLxxSzPZbmypGoqjCUrH7sVQGgONeTDQnBoSK7bSYUpn+y2NS7wm0EN486za+0J7wpssS86efOFzhaxi4NpCpdOPix9G/aFpU9BBde1DuQz7ROWSZbnOuFPlsuX3Tbc87I7m0pi73sHFmQY65hlW33TM6SeJOiHKfCxanEoAPxBl2iZRXUWZBqLTeWTwhqNtw48MrVVMt+b4suvq3O03pt88sRi0328wu2387dHvy0vRDEm2NwO9qXw/X/PKgC8W+XuE0pKgbKzxty4l9/hq3YnFZOfoqawVk0XK8F4x2dY2iTKtjOASYfbIy6baxSedvhP2otbWrV1PXotr2kajb6rkUHCeKh6mBzZVrlwOfIJsQGmPn4E5Gtft9muOq+fKaVunn3xx5eD9DKbFVlNIvoGFbmTHeoD1G+qu3QIrh/JHtlc1xsm3u+/P/75XB6vKL6XZfv1zHL2TIlfV+wL5zlua3hU7VvS8bf1J9zuzLcLqpU7yr0d91yI730ItdjWft04PgTSYNR/sjn3Zd9hNvsPQ5juaPrP4gbdVKIgIUJ4HkIa3VfS+DPP5X1QpetA5BUUPX8GmqZOz1e9ZjDBPs3ptm5bd/d8RLU0Xm+/VMqKznH8bFKbZEz1upVMlmw1DstoKOOvKe5/0EKxcsEurZfVS2/dSXLkCcFGU2ihMlFXkruy3p1/Z1P6ktQC79q5160nTVgvw5FpA/g2lvHTDuP1PPFuOXSuyeWJza2m6gk5nSy4xnGdLrOj40mzJdYtuZ0uuW5zOpzr9qplCHq6fY+rsoahxfOYiwZk+7aHaNW/huA2h2Ow0w/ZUVufO/qIhb4dkBqmws984+41TWcZ43MJvOIr8BtPV7b+nKTbt7P7Jj337Fw== \ No newline at end of file diff --git a/docs/drawio/drawDebt.drawio b/docs/drawio/drawDebt.drawio new file mode 100644 index 000000000..2653b64ad --- /dev/null +++ b/docs/drawio/drawDebt.drawio @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/docs/drawio/kick.drawio b/docs/drawio/kick.drawio new file mode 100644 index 000000000..c4758a438 --- /dev/null +++ b/docs/drawio/kick.drawio @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/docs/drawio/kickWithDeposit.drawio b/docs/drawio/kickWithDeposit.drawio new file mode 100644 index 000000000..6c1076aed --- /dev/null +++ b/docs/drawio/kickWithDeposit.drawio @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/docs/drawio/moveQuoteToken.drawio b/docs/drawio/moveQuoteToken.drawio new file mode 100644 index 000000000..5ec0cbef3 --- /dev/null +++ b/docs/drawio/moveQuoteToken.drawio @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/docs/drawio/poolContractsArchitecture.drawio b/docs/drawio/poolContractsArchitecture.drawio new file mode 100644 index 000000000..6000897e3 --- /dev/null +++ b/docs/drawio/poolContractsArchitecture.drawio @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/docs/drawio/removeCollateral.drawio b/docs/drawio/removeCollateral.drawio new file mode 100644 index 000000000..b1f8199cb --- /dev/null +++ b/docs/drawio/removeCollateral.drawio @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/docs/drawio/removeQuoteToken.drawio b/docs/drawio/removeQuoteToken.drawio new file mode 100644 index 000000000..dfe6768ca --- /dev/null +++ b/docs/drawio/removeQuoteToken.drawio @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/docs/drawio/repayDebt.drawio b/docs/drawio/repayDebt.drawio new file mode 100644 index 000000000..a9a221166 --- /dev/null +++ b/docs/drawio/repayDebt.drawio @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/docs/drawio/settle.drawio b/docs/drawio/settle.drawio new file mode 100644 index 000000000..30d3e9999 --- /dev/null +++ b/docs/drawio/settle.drawio @@ -0,0 +1 @@ +7V1bk9o4Fv41VHW2ii5b8gUe+zqbrZ5MVzqZZPdN2AK87Qtrm256fv1KtuSLLBsDNjgD/ZAgWTd0Pp1z9OnIjOCdt/ktRKvl74GN3RFQ7M0I3o8AADrUyH805yPNMadKmrEIHTvNUvOMF+cvzDJ5sbVj46hUMA4CN3ZW5Uwr8H1sxaU8FIbBe7nYPHDLva7QAlcyXizkVnN/OHa8ZLmqouQP/omdxZJ1PdHZgxmyXhdhsPZZf37g4/SJh3gzrGi0RHbwXsiCDyN4FwZBnH7yNnfYpdPKZyyt91jzNBtyiP24TQVzhmZItbSZBcy5NlPGIG3hDblrNg03ayt2Aj9iA44/+PyQsa/ox7XnPjlz7DrkW8LbFQ4dD8c4JE9clv2c592+L50Yv6yQRau+E+SQvGXsuSSlko9EmDEiVcIs7bpoFTmzpFeF5ITYWoeR84a/4ijFDM0N1jHt6S7DQlKUygDbrKlsrpWkXc+x2GcXzbB7mwntLnAD2n0iNlItDoPXDAG0oTkZ4yPyHJcC+08c2shHLJuhWCWCvEWus/BJwiKySL56VThMXm84jPGmkMWE9RsOyLSFH6QIX1WmkVZha2rCcPReACjH57KATZNjE7FFsciaztFBPjCAtAQLX6kFtFRQUpDtKnD8OOlfvx3p9wJYgjBeBovAR24RLrkIlWGKsHYFtZapppdECnRNItOqSIFh9CBSWJHoM9G5d4HnXVTAMFSAxpdyZlaPqAK0RwdCB/75Za6orz9jc/ZqTcfaRQvUrqN9tYAhU+xVoaqgL6FC4yLVzqWqG+10eydirYxetk4NN2ZzQz4v6Od7vAoih8iSPSPdFB9fbMAAbIAByjbgqG6gVF2oFyNQv+D2VRdSsValanQg1D/+8/nn2vtjM36GPzYz48e/Pr7+HKv6eQq1IrA2cm7t30/NVlKFkx5MwETq3l90+gB0eqawB7G1n57nym9c5pNdRbqfT2/04/ypEokWvb+CaI3/rQP+YJyuxBtSQFVWm2Sm+HPKqqarPc/7hpaBh4plUr/xYUPmmUCA1HGdWYjo10m7n4W5b7mby+kHMd6ud5gqIR4KvKWycyzk3jDxx8GqAAYXz2l/EWnK8Rff6LP7sXFirHZPGsKWmgXqfeCwyjBfcHgeOBSZq9PisMp0HhWHn/0LDk+EQ4NXOQEOpZvnSUWi2F7gF5bM3aeHPFcQQV7mKUjERKfzvziOP9jEoXUclGFQmmyG0vJc04bJLIcfP1kvSeLfNHGtTAHPuN8UH99/FFMFhy/JrJVeFKxDtnJqndEYhQscN8wj22/TyWvEQYhdFJNtRmkEMqmyqs/UvS3osWlZj6miB54OlNXKsXEThuijUIx5zbX9ZN5h1o9abG7X8uRDOoIcqNmcHED81PuVtvPGVRayrHCNE52Ho7ig1wplJNWu4iUpvwxc+zl0CDzAHYUBQdQD3Tli+1NdS5Ll9ESVUXkJVDYBojr0HNtOVxvdkKJ8p1rd8IgLR768qvhvUggSHoIFO7ChjIpRAzJVNyYrdQLYujgQ9gIfEcznEY5Hou7rAlKKVICD0Ic2ipYZ7YB9+4YGmtBBrbCf5jw69Ns2a7v+NRQoaQKgw3ITqbqtaKhKQ6puNjfUlapT5P3Uqrrm8j2puqrLeOXj98/+nAiLeDaJdko1nc1V3adfXhdlQR4HKyPlWgUTrSS3sdqJajqibjKHq5sK6shyURRRP7ygkdRTaiRdKXNhqik40201kmFsaagjjaQLm1XeT924tpTvSSNV+Xxv7cZXfwOtY3boAqmgjBl+jt+NQ8Qb1Y6lgzg/3Oxyp5GsQHnE/rtjvZJP30KMk20u2eUSN3pJE8R+0e4yx5zbr9YueiNr0Vx1vbJRTAcRkaE6/mKUxO0mqzSKkydtmz5fPqTRzdyBEpmUHUbVYEukyInw44wiJ9JbQAEAJze09TO+zfHHGyempAnVPSyZ0iZTotfSdM6a0MRHISFyJqUtR62siwRKY4TGUFgUXWArgNqTIdfk/dQa8uby/RhyAH8NtA/Or4RCJBTUzF5QpE7l/dRunJvLl1Ek2Xbrzd+qxlvuDI71oYIFu0vcm9jFNIDkHs92IPVe0nooRF70aW8jf2qPNl8vLV1aUBc9updLq6uCTws68Wmn5UaF+v35tLCKOBtbIfZwMroVARkRLfItHF3HShFvOXC2VPjss6tMO9RcuRRm1FMjs4pDVB8qdfE12/qaY4Gc1CfVgLjsrK3oa2paPZoPi3XXT2F9S35dR47nYOywPt2T3xFNX6WhGoPemSI6CcGXH7NeZ0l20KqP9jhnPYJnt3UfwW+Q9A09ETGGuCetgd7OZxTCxoD3U3tG0Vy+n40ElAVu1jAv/IzipUi2bHffqMPHqtADj6fvz2fkxMHOeEnlGk51ubt14IIYm8IhXY9u20lCV/qwnJyy6YKxqdWZW0maY+lMMYTF1AXfal9zXWlo5/131xDVWrHlGRdtJ1sLBfl2qtvIv/9IFne+A1DCtOjD7zfRSOCqj8KXc7r+a5pMx1rM/M5L0liZA3o8oCr2HDqP30uWho7tan9rcdlr7brXEklWQ3YHQcbrT/vi9RvuIcihzoMrdl8kac0918IFeofeZxIO6k0e+rENerA36FV946cAXV5rMYjrbxOBQDBltySPeqVZl2kqASl/9ztwzeto34twGpSogp6uwMpFC2qN0OXyx3AtE+hEzwzs8gffMA396PfIgQ4FRnQ6KjGihp497+TqSd977YlRRpyu6dcK2G+7DaFwdqIZ1woE0+zvwM13Xa+G2KsA+x6YTL368gnHj8g6T2nHzM+newMiIi94oymXOHTnrCI7ct4nE+E4h1/z2aYmtYblcdC7SOotdmHbNguossKhQNi03DoWatu7BDSkcRD2fp3mlW3pofYvSqS35sclQK1FJYDmtXg62OD917DjQtxEH2G7UgTXXzguCDbEdOYSF1Mm9y8Bi2HIWbTCU/bsSxDfuRiFdKav2l6RO0sdueVFP81ohFAI0uIB4ceImZUi7CSeZN0NYVWf7OOmbb/qAliaQ2EEoJH8jXY40BacvlZyPzREAgp+oNouqE/iUOpiQ9eKAnt2AjVTdAmaIyC3lD/YaZSugOpLK1M/cfg3YnoxmPoUllWUKli7FlfrSg0ApY+jZfnL51rFoGbbgNx5O/TIKg1PrTecNS5cbXDhxQAfyQDrnNbbesLQAa8ox6xxCgt8PiYTToR3nre/Nao0N9STkeT9tDWSQvl+jGR2t7JgJX16Bw/bXxMe5VytpfAGRnUiGLshW8vTv0OoExYZjEosMuCs8rBY5O2qsBzr1fB23R7o5jKKNRUcQDerQlvwKHSzLn6DLaMUy3f7MhC59FoF2ma8NPLIWqNjnoeBN6reyj5i9FgyYhq4xi5bX/zVI/mr4iVraEh+usOUkOp6X/4qqHKSt2vrFceXmJjjvEq8GTBTvfyKCf4e16PExMgBc6av+d8ixobF1TYmBpqyE7aqbHvbu4LqeeslJmb4tgl0omfK91CAxC71FRMjx+JJeJTLSUZrWkbYYuj7XvZU+M4kb6nTo4yut9mgSp0gu3A9/FyZEyFcpPGIc2DMCcfyL8mcDIPs6FzBEL0gaBiRits3YI40dAQGQwxm58OvPdFtLt8Pg8G5nbYHbMnG8Lp0ES69ecaeuKtodzLCJXjGYVqXNcey7PR36L5dbu0ck6NQFRGKsCVJscevkJFk/lPXKazznxKHD/8H \ No newline at end of file diff --git a/docs/drawio/take.drawio b/docs/drawio/take.drawio new file mode 100644 index 000000000..f8a6de366 --- /dev/null +++ b/docs/drawio/take.drawio @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/docs/html/addCollateral.html b/docs/html/addCollateral.html new file mode 100644 index 000000000..2e0301cd4 --- /dev/null +++ b/docs/html/addCollateral.html @@ -0,0 +1,11 @@ + + + + +addCollateral + + +
+ + + \ No newline at end of file diff --git a/docs/html/addQuoteToken.html b/docs/html/addQuoteToken.html new file mode 100644 index 000000000..11fd15bda --- /dev/null +++ b/docs/html/addQuoteToken.html @@ -0,0 +1,11 @@ + + + + +addQuoteToken + + +
+ + + \ No newline at end of file diff --git a/docs/html/bucketTake.html b/docs/html/bucketTake.html new file mode 100644 index 000000000..ff4d63be4 --- /dev/null +++ b/docs/html/bucketTake.html @@ -0,0 +1,11 @@ + + + + +bucketTake + + +
+ + + \ No newline at end of file diff --git a/docs/html/components.html b/docs/html/components.html new file mode 100644 index 000000000..08bd8ff7a --- /dev/null +++ b/docs/html/components.html @@ -0,0 +1,11 @@ + + + + +components + + +
+ + + \ No newline at end of file diff --git a/docs/html/drawDebt.html b/docs/html/drawDebt.html new file mode 100644 index 000000000..1e57c7099 --- /dev/null +++ b/docs/html/drawDebt.html @@ -0,0 +1,11 @@ + + + + +drawDebt + + +
+ + + \ No newline at end of file diff --git a/docs/html/kick.html b/docs/html/kick.html new file mode 100644 index 000000000..af55df35c --- /dev/null +++ b/docs/html/kick.html @@ -0,0 +1,11 @@ + + + + +kick + + +
+ + + \ No newline at end of file diff --git a/docs/html/kickWithDeposit.html b/docs/html/kickWithDeposit.html new file mode 100644 index 000000000..45b15fa7b --- /dev/null +++ b/docs/html/kickWithDeposit.html @@ -0,0 +1,11 @@ + + + + +kickWithDeposit + + +
+ + + \ No newline at end of file diff --git a/docs/html/moveQuoteToken.html b/docs/html/moveQuoteToken.html new file mode 100644 index 000000000..4df0727f6 --- /dev/null +++ b/docs/html/moveQuoteToken.html @@ -0,0 +1,11 @@ + + + + +moveQuoteToken + + +
+ + + \ No newline at end of file diff --git a/docs/html/poolContractsArchitecture.html b/docs/html/poolContractsArchitecture.html new file mode 100644 index 000000000..4bd8403d6 --- /dev/null +++ b/docs/html/poolContractsArchitecture.html @@ -0,0 +1,11 @@ + + + + +Untitled Diagram-Page-1 + + +
+ + + \ No newline at end of file diff --git a/docs/html/removeCollateral.html b/docs/html/removeCollateral.html new file mode 100644 index 000000000..b7622c8af --- /dev/null +++ b/docs/html/removeCollateral.html @@ -0,0 +1,11 @@ + + + + +removeCollateral + + +
+ + + \ No newline at end of file diff --git a/docs/html/removeQuoteToken.html b/docs/html/removeQuoteToken.html new file mode 100644 index 000000000..5c04479b9 --- /dev/null +++ b/docs/html/removeQuoteToken.html @@ -0,0 +1,11 @@ + + + + +removeQuoteToken + + +
+ + + \ No newline at end of file diff --git a/docs/html/repayDebt.html b/docs/html/repayDebt.html new file mode 100644 index 000000000..02d7c2d78 --- /dev/null +++ b/docs/html/repayDebt.html @@ -0,0 +1,11 @@ + + + + +repayDebt + + +
+ + + \ No newline at end of file diff --git a/docs/html/settle.html b/docs/html/settle.html new file mode 100644 index 000000000..e8fedd42e --- /dev/null +++ b/docs/html/settle.html @@ -0,0 +1,11 @@ + + + + +addCollateral + + +
+ + + \ No newline at end of file diff --git a/docs/html/take.html b/docs/html/take.html new file mode 100644 index 000000000..308899a7d --- /dev/null +++ b/docs/html/take.html @@ -0,0 +1,11 @@ + + + + +take + + +
+ + + \ No newline at end of file diff --git a/docs/jpeg/ajnaContractsArchitecture.jpeg b/docs/jpeg/ajnaContractsArchitecture.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..7a4ae3937199e9e058a5069861b7656a26db5da8 GIT binary patch literal 83293 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

6oVID^`x_l^ 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 M_@9jh_dl=y1xXK_l>h($ literal 0 HcmV?d00001 diff --git a/docs/jpeg/poolContract.jpeg b/docs/jpeg/poolContract.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..5612d316d5abcf1bc82de73b6ac9e07771b02031 GIT binary patch literal 51883 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 literal 0 HcmV?d00001 diff --git a/docs/jpeg/poolContractsArchitecture.jpeg b/docs/jpeg/poolContractsArchitecture.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..03048a0b379f6cd021bf0f7ac0091fe5032240b2 GIT binary patch literal 81800 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 literal 0 HcmV?d00001 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/ERC20Pool.sol b/src/ERC20Pool.sol index 4f90e78c8..f139b8ce5 100644 --- a/src/ERC20Pool.sol +++ b/src/ERC20Pool.sol @@ -15,8 +15,7 @@ import { IERC20Taker } from './interfaces/pool/erc20/IERC20Taker.sol'; import { IPoolLenderActions, - IPoolLiquidationActions, - IERC20Token + IPoolLiquidationActions } from './interfaces/pool/IPool.sol'; import { IERC3156FlashBorrower, @@ -39,7 +38,10 @@ import { _roundToScale, _roundUpToScale } from './libraries/helpers/PoolHelper.sol'; -import { _revertIfAuctionClearable } from './libraries/helpers/RevertsHelper.sol'; +import { + _revertIfAuctionClearable, + _revertOnExpiry +} from './libraries/helpers/RevertsHelper.sol'; import { Loans } from './libraries/internal/Loans.sol'; import { Deposits } from './libraries/internal/Deposits.sol'; @@ -165,7 +167,7 @@ contract ERC20Pool is FlashloanablePool, IERC20Pool { if (amountToBorrow_ != 0) { // update pool balances state - poolBalances.t0Debt += result.t0DebtChange; + poolBalances.t0Debt = result.t0PoolDebt; // move borrowed amount from pool to sender _transferQuoteToken(msg.sender, amountToBorrow_); @@ -184,7 +186,9 @@ contract ERC20Pool is FlashloanablePool, IERC20Pool { function repayDebt( address borrowerAddress_, uint256 maxQuoteTokenAmountToRepay_, - uint256 collateralAmountToPull_ + uint256 collateralAmountToPull_, + address collateralReceiver_, + uint256 limitIndex_ ) external nonReentrant { PoolState memory poolState = _accruePoolInterest(); @@ -200,7 +204,8 @@ contract ERC20Pool is FlashloanablePool, IERC20Pool { poolState, borrowerAddress_, maxQuoteTokenAmountToRepay_, - collateralAmountToPull_ + collateralAmountToPull_, + limitIndex_ ); emit RepayDebt(borrowerAddress_, result.quoteTokenToRepay, collateralAmountToPull_, result.newLup); @@ -212,7 +217,7 @@ contract ERC20Pool is FlashloanablePool, IERC20Pool { if (result.quoteTokenToRepay != 0) { // update pool balances state - poolBalances.t0Debt -= result.t0RepaidDebt; + poolBalances.t0Debt = result.t0PoolDebt; if (result.t0DebtInAuctionChange != 0) { poolBalances.t0DebtInAuction -= result.t0DebtInAuctionChange; } @@ -224,52 +229,8 @@ contract ERC20Pool is FlashloanablePool, IERC20Pool { // update pool balances state poolBalances.pledgedCollateral = result.poolCollateral; - // move collateral from pool to sender - _transferCollateral(msg.sender, collateralAmountToPull_); - } - } - - /************************************/ - /*** Flashloan External Functions ***/ - /************************************/ - - /// @inheritdoc FlashloanablePool - function flashLoan( - IERC3156FlashBorrower receiver_, - address token_, - uint256 amount_, - bytes calldata data_ - ) external override(IERC3156FlashLender, FlashloanablePool) nonReentrant returns (bool) { - if (token_ == _getArgAddress(QUOTE_ADDRESS)) return _flashLoanQuoteToken(receiver_, token_, amount_, data_); - - if (token_ == _getArgAddress(COLLATERAL_ADDRESS)) { - _transferCollateral(address(receiver_), amount_); - - if (receiver_.onFlashLoan(msg.sender, token_, amount_, 0, data_) != - keccak256("ERC3156FlashBorrower.onFlashLoan")) revert FlashloanCallbackFailed(); - - _transferCollateralFrom(address(receiver_), amount_); - return true; - } - - revert FlashloanUnavailableForToken(); - } - - /// @inheritdoc FlashloanablePool - function flashFee( - address token_, - uint256 - ) external pure override(IERC3156FlashLender, FlashloanablePool) returns (uint256) { - if (token_ == _getArgAddress(QUOTE_ADDRESS) || token_ == _getArgAddress(COLLATERAL_ADDRESS)) return 0; - revert FlashloanUnavailableForToken(); - } - - /// @inheritdoc FlashloanablePool - function maxFlashLoan( - address token_ - ) external view override(IERC3156FlashLender, FlashloanablePool) returns (uint256 maxLoan_) { - if (token_ == _getArgAddress(QUOTE_ADDRESS) || token_ == _getArgAddress(COLLATERAL_ADDRESS)) { - maxLoan_ = IERC20Token(token_).balanceOf(address(this)); + // move collateral from pool to address specified as collateral receiver + _transferCollateral(collateralReceiver_, collateralAmountToPull_); } } @@ -286,8 +247,10 @@ contract ERC20Pool is FlashloanablePool, IERC20Pool { */ function addCollateral( uint256 amountToAdd_, - uint256 index_ + uint256 index_, + uint256 expiry_ ) external override nonReentrant returns (uint256 bucketLPs_) { + _revertOnExpiry(expiry_); PoolState memory poolState = _accruePoolInterest(); // revert if the dust amount was not exceeded, but round on the scale amount @@ -359,12 +322,11 @@ contract ERC20Pool is FlashloanablePool, IERC20Pool { ) external override nonReentrant { PoolState memory poolState = _accruePoolInterest(); - uint256 assets = Maths.wmul(poolBalances.t0Debt, poolState.inflator) + _getPoolQuoteTokenBalance(); + uint256 assets = Maths.wmul(poolBalances.t0Debt, poolState.inflator) + _getNormalizedPoolQuoteTokenBalance(); uint256 liabilities = Deposits.treeSum(deposits) + auctions.totalBondEscrowed + reserveAuction.unclaimed; ( - , , uint256 collateralSettled, uint256 t0DebtSettled @@ -427,18 +389,11 @@ contract ERC20Pool is FlashloanablePool, IERC20Pool { result.quoteTokenAmount = _roundUpToScale(result.quoteTokenAmount, _getArgUint256(QUOTE_SCALE)); // update pool balances state - uint256 t0PoolDebt = poolBalances.t0Debt; uint256 t0DebtInAuction = poolBalances.t0DebtInAuction; - - if (result.t0DebtPenalty != 0) { - t0PoolDebt += result.t0DebtPenalty; - t0DebtInAuction += result.t0DebtPenalty; - } - - t0PoolDebt -= result.t0RepayAmount; + t0DebtInAuction += result.t0DebtPenalty; t0DebtInAuction -= result.t0DebtInAuctionChange; - poolBalances.t0Debt = t0PoolDebt; + poolBalances.t0Debt = result.t0PoolDebt; poolBalances.t0DebtInAuction = t0DebtInAuction; poolBalances.pledgedCollateral -= result.collateralAmount; @@ -457,7 +412,7 @@ contract ERC20Pool is FlashloanablePool, IERC20Pool { ); } - _transferQuoteTokenFrom(callee_, result.quoteTokenAmount); + _transferQuoteTokenFrom(msg.sender, result.quoteTokenAmount); } /** @@ -488,18 +443,11 @@ contract ERC20Pool is FlashloanablePool, IERC20Pool { ); // update pool balances state - uint256 t0PoolDebt = poolBalances.t0Debt; uint256 t0DebtInAuction = poolBalances.t0DebtInAuction; - - if (result.t0DebtPenalty != 0) { - t0PoolDebt += result.t0DebtPenalty; - t0DebtInAuction += result.t0DebtPenalty; - } - - t0PoolDebt -= result.t0RepayAmount; + t0DebtInAuction += result.t0DebtPenalty; t0DebtInAuction -= result.t0DebtInAuctionChange; - poolBalances.t0Debt = t0PoolDebt; + poolBalances.t0Debt = result.t0PoolDebt; poolBalances.t0DebtInAuction = t0DebtInAuction; poolBalances.pledgedCollateral -= result.collateralAmount; @@ -509,6 +457,20 @@ contract ERC20Pool is FlashloanablePool, IERC20Pool { _updateInterestState(poolState, result.newLup); } + /***************************/ + /*** Flashloan Functions ***/ + /***************************/ + + /** + * @inheritdoc FlashloanablePool + * @dev Override default implementation and allows flashloans for both quote and collateral token. + */ + function _isFlashloanSupported( + address token_ + ) internal virtual view override returns (bool) { + return token_ == _getArgAddress(QUOTE_ADDRESS) || token_ == _getArgAddress(COLLATERAL_ADDRESS); + } + /************************/ /*** Helper Functions ***/ /************************/ diff --git a/src/ERC20PoolFactory.sol b/src/ERC20PoolFactory.sol index c6ed65b41..6098ea682 100644 --- a/src/ERC20PoolFactory.sol +++ b/src/ERC20PoolFactory.sol @@ -4,8 +4,8 @@ pragma solidity 0.8.14; import { ClonesWithImmutableArgs } from '@clones/ClonesWithImmutableArgs.sol'; -import { IERC20PoolFactory } from './interfaces/pool/erc20/IERC20PoolFactory.sol'; - +import { IERC20PoolFactory } from './interfaces/pool/erc20/IERC20PoolFactory.sol'; +import { IPoolFactory } from './interfaces/pool/IPoolFactory.sol'; import { IERC20Token, PoolType } from './interfaces/pool/IPool.sol'; import { ERC20Pool } from './ERC20Pool.sol'; @@ -49,7 +49,9 @@ contract ERC20PoolFactory is PoolDeployer, IERC20PoolFactory { */ function deployPool( address collateral_, address quote_, uint256 interestRate_ - ) external canDeploy(ERC20_NON_SUBSET_HASH, collateral_, quote_, interestRate_) returns (address pool_) { + ) external canDeploy(collateral_, quote_, interestRate_) returns (address pool_) { + if (deployedPools[ERC20_NON_SUBSET_HASH][collateral_][quote_] != address(0)) revert IPoolFactory.PoolAlreadyExists(); + uint256 quoteTokenScale = 10 ** (18 - IERC20Token(quote_).decimals()); uint256 collateralScale = 10 ** (18 - IERC20Token(collateral_).decimals()); diff --git a/src/ERC721Pool.sol b/src/ERC721Pool.sol index b1bf36b9e..386055912 100644 --- a/src/ERC721Pool.sol +++ b/src/ERC721Pool.sol @@ -24,15 +24,13 @@ import { IERC721PoolLenderActions } from './interfaces/pool/erc721/IERC721Pool.sol'; import { IERC721Taker } from './interfaces/pool/erc721/IERC721Taker.sol'; -import { - ICryptoPunks, - ICryptoKitties, - NFTTypes -} from './interfaces/pool/erc721/IERC721NonStandard.sol'; import { FlashloanablePool } from './base/FlashloanablePool.sol'; -import { _revertIfAuctionClearable } from './libraries/helpers/RevertsHelper.sol'; +import { + _revertIfAuctionClearable, + _revertOnExpiry +} from './libraries/helpers/RevertsHelper.sol'; import { Maths } from './libraries/internal/Maths.sol'; import { Deposits } from './libraries/internal/Deposits.sol'; @@ -64,7 +62,6 @@ contract ERC721Pool is FlashloanablePool, IERC721Pool { // immutable args offset uint256 internal constant SUBSET = 93; - uint256 internal constant NFT_TYPE = 125; /***********************/ /*** State Variables ***/ @@ -163,7 +160,7 @@ contract ERC721Pool is FlashloanablePool, IERC721Pool { if (result.t0DebtInAuctionChange != 0) { poolBalances.t0DebtInAuction -= result.t0DebtInAuctionChange; } - poolBalances.pledgedCollateral += Maths.wad(tokenIdsToPledge_.length); + poolBalances.pledgedCollateral = result.poolCollateral; // move collateral from sender to pool _transferFromSenderToPool(borrowerTokenIds[borrowerAddress_], tokenIdsToPledge_); @@ -174,7 +171,7 @@ contract ERC721Pool is FlashloanablePool, IERC721Pool { // move borrowed amount from pool to sender if (amountToBorrow_ != 0) { // update pool balances state - poolBalances.t0Debt += result.t0DebtChange; + poolBalances.t0Debt = result.t0PoolDebt; // move borrowed amount from pool to sender _transferQuoteToken(msg.sender, amountToBorrow_); @@ -187,14 +184,16 @@ contract ERC721Pool is FlashloanablePool, IERC721Pool { * - decrement poolBalances.t0Debt accumulator * - decrement poolBalances.t0DebtInAuction accumulator * - decrement poolBalances.pledgedCollateral accumulator - * - update borrowerTokenIds arrays + * - update borrowerTokenIds and bucketTokenIds arrays * @dev emit events: * - RepayDebt */ function repayDebt( address borrowerAddress_, uint256 maxQuoteTokenAmountToRepay_, - uint256 noOfNFTsToPull_ + uint256 noOfNFTsToPull_, + address collateralReceiver_, + uint256 limitIndex_ ) external nonReentrant { PoolState memory poolState = _accruePoolInterest(); @@ -206,7 +205,8 @@ contract ERC721Pool is FlashloanablePool, IERC721Pool { poolState, borrowerAddress_, maxQuoteTokenAmountToRepay_, - Maths.wad(noOfNFTsToPull_) + Maths.wad(noOfNFTsToPull_), + limitIndex_ ); emit RepayDebt(borrowerAddress_, result.quoteTokenToRepay, noOfNFTsToPull_, result.newLup); @@ -218,9 +218,12 @@ contract ERC721Pool is FlashloanablePool, IERC721Pool { poolState.collateral = result.poolCollateral; _updateInterestState(poolState, result.newLup); + // update pool balances state + poolBalances.pledgedCollateral = result.poolCollateral; + if (result.quoteTokenToRepay != 0) { // update pool balances state - poolBalances.t0Debt -= result.t0RepaidDebt; + poolBalances.t0Debt = result.t0PoolDebt; if (result.t0DebtInAuctionChange != 0) { poolBalances.t0DebtInAuction -= result.t0DebtInAuctionChange; } @@ -229,11 +232,8 @@ contract ERC721Pool is FlashloanablePool, IERC721Pool { _transferQuoteTokenFrom(msg.sender, result.quoteTokenToRepay); } if (noOfNFTsToPull_ != 0) { - // update pool balances state - poolBalances.pledgedCollateral = result.poolCollateral; - - // move collateral from pool to sender - _transferFromPoolToAddress(msg.sender, borrowerTokenIds[msg.sender], noOfNFTsToPull_); + // move collateral from pool to address specified as collateral receiver + _transferFromPoolToAddress(collateralReceiver_, borrowerTokenIds[msg.sender], noOfNFTsToPull_); } } @@ -244,14 +244,16 @@ contract ERC721Pool is FlashloanablePool, IERC721Pool { /** * @inheritdoc IERC721PoolLenderActions * @dev write state: - * - update borrowerTokenIds arrays + * - update bucketTokenIds arrays * @dev emit events: * - AddCollateralNFT */ function addCollateral( uint256[] calldata tokenIdsToAdd_, - uint256 index_ + uint256 index_, + uint256 expiry_ ) external override nonReentrant returns (uint256 bucketLPs_) { + _revertOnExpiry(expiry_); PoolState memory poolState = _accruePoolInterest(); bucketLPs_ = LenderActions.addCollateral( @@ -282,6 +284,8 @@ contract ERC721Pool is FlashloanablePool, IERC721Pool { uint256 noOfNFTsToRemove_, uint256 toIndex_ ) external override nonReentrant returns (uint256 collateralMerged_, uint256 bucketLPs_) { + _revertIfAuctionClearable(auctions, loans); + PoolState memory poolState = _accruePoolInterest(); uint256 collateralAmount = Maths.wad(noOfNFTsToRemove_); @@ -356,7 +360,8 @@ contract ERC721Pool is FlashloanablePool, IERC721Pool { ) external nonReentrant override { PoolState memory poolState = _accruePoolInterest(); - uint256 assets = Maths.wmul(poolBalances.t0Debt, poolState.inflator) + _getPoolQuoteTokenBalance(); + uint256 assets = Maths.wmul(poolBalances.t0Debt, poolState.inflator) + _getNormalizedPoolQuoteTokenBalance(); + uint256 liabilities = Deposits.treeSum(deposits) + auctions.totalBondEscrowed + reserveAuction.unclaimed; SettleParams memory params = SettleParams( @@ -370,7 +375,6 @@ contract ERC721Pool is FlashloanablePool, IERC721Pool { ); ( uint256 collateralRemaining, - uint256 t0DebtRemaining, uint256 collateralSettled, uint256 t0DebtSettled ) = Auctions.settlePoolDebt( @@ -381,8 +385,7 @@ contract ERC721Pool is FlashloanablePool, IERC721Pool { params ); - // slither-disable-next-line incorrect-equality - if (t0DebtRemaining == 0) _rebalanceTokens(params.borrower, collateralRemaining); + if (collateralSettled > 0) _rebalanceTokens(params.borrower, collateralRemaining); // update pool balances state poolBalances.t0Debt -= t0DebtSettled; @@ -422,24 +425,20 @@ contract ERC721Pool is FlashloanablePool, IERC721Pool { ); // update pool balances state - uint256 t0PoolDebt = poolBalances.t0Debt; uint256 t0DebtInAuction = poolBalances.t0DebtInAuction; - - if (result.t0DebtPenalty != 0) { - t0PoolDebt += result.t0DebtPenalty; - t0DebtInAuction += result.t0DebtPenalty; - } - - t0PoolDebt -= result.t0RepayAmount; + t0DebtInAuction += result.t0DebtPenalty; t0DebtInAuction -= result.t0DebtInAuctionChange; - poolBalances.t0Debt = t0PoolDebt; - poolBalances.t0DebtInAuction = t0DebtInAuction; - poolBalances.pledgedCollateral -= result.collateralAmount; + poolBalances.t0Debt = result.t0PoolDebt; + poolBalances.t0DebtInAuction = t0DebtInAuction; + + // the total collateral taken from borrower pledged collateral (collateral taken plus collateral compensated if auction settled) + uint256 collateralSettled = result.collateralAmount + result.compensatedCollateral; + poolBalances.pledgedCollateral -= collateralSettled; // update pool interest rate state poolState.debt = result.poolDebt; - poolState.collateral -= result.collateralAmount; + poolState.collateral -= collateralSettled; _updateInterestState(poolState, result.newLup); // transfer rounded collateral from pool to taker @@ -449,10 +448,12 @@ contract ERC721Pool is FlashloanablePool, IERC721Pool { result.collateralAmount / 1e18 ); + uint256 totalQuoteTokenAmount = result.quoteTokenAmount + result.excessQuoteToken; + if (data_.length != 0) { IERC721Taker(callee_).atomicSwapCallback( tokensTaken, - result.quoteTokenAmount / _getArgUint256(QUOTE_SCALE), + totalQuoteTokenAmount / _getArgUint256(QUOTE_SCALE), data_ ); } @@ -460,7 +461,7 @@ contract ERC721Pool is FlashloanablePool, IERC721Pool { if (result.settledAuction) _rebalanceTokens(borrowerAddress_, result.remainingCollateral); // transfer from taker to pool the amount of quote tokens needed to cover collateral auctioned (including excess for rounded collateral) - _transferQuoteTokenFrom(callee_, result.quoteTokenAmount + result.excessQuoteToken); + _transferQuoteTokenFrom(msg.sender, totalQuoteTokenAmount); // transfer from pool to borrower the excess of quote tokens after rounding collateral auctioned if (result.excessQuoteToken != 0) _transferQuoteToken(borrowerAddress_, result.excessQuoteToken); @@ -494,24 +495,20 @@ contract ERC721Pool is FlashloanablePool, IERC721Pool { ); // update pool balances state - uint256 t0PoolDebt = poolBalances.t0Debt; uint256 t0DebtInAuction = poolBalances.t0DebtInAuction; - - if (result.t0DebtPenalty != 0) { - t0PoolDebt += result.t0DebtPenalty; - t0DebtInAuction += result.t0DebtPenalty; - } - - t0PoolDebt -= result.t0RepayAmount; + t0DebtInAuction += result.t0DebtPenalty; t0DebtInAuction -= result.t0DebtInAuctionChange; - poolBalances.t0Debt = t0PoolDebt; - poolBalances.t0DebtInAuction = t0DebtInAuction; - poolBalances.pledgedCollateral -= result.collateralAmount; + poolBalances.t0Debt = result.t0PoolDebt; + poolBalances.t0DebtInAuction = t0DebtInAuction; + + // the total collateral taken from borrower pledged collateral (collateral taken plus collateral compensated if auction settled) + uint256 collateralSettled = result.collateralAmount + result.compensatedCollateral; + poolBalances.pledgedCollateral -= collateralSettled; // update pool interest rate state poolState.debt = result.poolDebt; - poolState.collateral -= result.collateralAmount; + poolState.collateral -= collateralSettled; _updateInterestState(poolState, result.newLup); if (result.settledAuction) _rebalanceTokens(borrowerAddress_, result.remainingCollateral); @@ -560,22 +557,13 @@ contract ERC721Pool is FlashloanablePool, IERC721Pool { uint256[] calldata tokenIds_ ) internal { bool subset = _getArgUint256(SUBSET) != 0; - uint8 nftType = _getArgUint8(NFT_TYPE); for (uint256 i = 0; i < tokenIds_.length;) { uint256 tokenId = tokenIds_[i]; if (subset && !tokenIdsAllowed[tokenId]) revert OnlySubset(); poolTokens_.push(tokenId); - if (nftType == uint8(NFTTypes.STANDARD_ERC721)){ - _transferNFT(msg.sender, address(this), tokenId); - } - else if (nftType == uint8(NFTTypes.CRYPTOKITTIES)) { - ICryptoKitties(_getArgAddress(COLLATERAL_ADDRESS)).transferFrom(msg.sender ,address(this), tokenId); - } - else{ - ICryptoPunks(_getArgAddress(COLLATERAL_ADDRESS)).buyPunk(tokenId); - } + _transferNFT(msg.sender, address(this), tokenId); unchecked { ++i; } } @@ -598,21 +586,11 @@ contract ERC721Pool is FlashloanablePool, IERC721Pool { uint256 noOfNFTsInPool = poolTokens_.length; - uint8 nftType = _getArgUint8(NFT_TYPE); - for (uint256 i = 0; i < amountToRemove_;) { uint256 tokenId = poolTokens_[--noOfNFTsInPool]; // start with transferring the last token added in bucket poolTokens_.pop(); - if (nftType == uint8(NFTTypes.STANDARD_ERC721)){ - _transferNFT(address(this), toAddress_, tokenId); - } - else if (nftType == uint8(NFTTypes.CRYPTOKITTIES)) { - ICryptoKitties(_getArgAddress(COLLATERAL_ADDRESS)).transfer(toAddress_, tokenId); - } - else { - ICryptoPunks(_getArgAddress(COLLATERAL_ADDRESS)).transferPunk(toAddress_, tokenId); - } + _transferNFT(address(this), toAddress_, tokenId); tokensTransferred[i] = tokenId; @@ -624,13 +602,14 @@ contract ERC721Pool is FlashloanablePool, IERC721Pool { /** * @dev Helper function to transfer an NFT from owner to target address (reused in code to reduce contract deployment bytecode size). + * @dev Since transferFrom is used instead of safeTransferFrom, calling smart contracts must be careful to check that they support any received NFTs. * @param from_ NFT owner address. * @param to_ New NFT owner address. * @param tokenId_ NFT token id to be transferred. */ function _transferNFT(address from_, address to_, uint256 tokenId_) internal { // slither-disable-next-line calls-loop - IERC721Token(_getArgAddress(COLLATERAL_ADDRESS)).safeTransferFrom(from_, to_, tokenId_); + IERC721Token(_getArgAddress(COLLATERAL_ADDRESS)).transferFrom(from_, to_, tokenId_); } /************************/ diff --git a/src/ERC721PoolFactory.sol b/src/ERC721PoolFactory.sol index 98301a5ce..ef1da0440 100644 --- a/src/ERC721PoolFactory.sol +++ b/src/ERC721PoolFactory.sol @@ -5,9 +5,8 @@ pragma solidity 0.8.14; import { ClonesWithImmutableArgs } from '@clones/ClonesWithImmutableArgs.sol'; import { IERC165 } from '@openzeppelin/contracts/utils/introspection/IERC165.sol'; -import { IERC721PoolFactory } from './interfaces/pool/erc721/IERC721PoolFactory.sol'; -import { NFTTypes } from './interfaces/pool/erc721/IERC721NonStandard.sol'; - +import { IERC721PoolFactory } from './interfaces/pool/erc721/IERC721PoolFactory.sol'; +import { IPoolFactory } from './interfaces/pool/IPoolFactory.sol'; import { IERC20Token, PoolType } from './interfaces/pool/IPool.sol'; import { ERC721Pool } from './ERC721Pool.sol'; @@ -53,29 +52,16 @@ contract ERC721PoolFactory is PoolDeployer, IERC721PoolFactory { */ function deployPool( address collateral_, address quote_, uint256[] memory tokenIds_, uint256 interestRate_ - ) external canDeploy(getNFTSubsetHash(tokenIds_), collateral_, quote_, interestRate_) returns (address pool_) { - uint256 quoteTokenScale = 10**(18 - IERC20Token(quote_).decimals()); + ) external canDeploy(collateral_, quote_, interestRate_) returns (address pool_) { + bytes32 subsetHash = getNFTSubsetHash(tokenIds_); + if (deployedPools[subsetHash][collateral_][quote_] != address(0)) revert IPoolFactory.PoolAlreadyExists(); - NFTTypes nftType; - // CryptoPunks NFTs - if (collateral_ == 0xb47e3cd837dDF8e4c57F05d70Ab865de6e193BBB ) { - nftType = NFTTypes.CRYPTOPUNKS; - } - // CryptoKitties and CryptoFighters NFTs - else if (collateral_ == 0x06012c8cf97BEaD5deAe237070F9587f8E7A266d || collateral_ == 0x87d598064c736dd0C712D329aFCFAA0Ccc1921A1) { - nftType = NFTTypes.CRYPTOKITTIES; - } - // All other NFTs that support the EIP721 standard - else { - // Here 0x80ac58cd is the ERC721 interface Id - // Neither a standard NFT nor a non-standard supported NFT(punk, kitty or fighter) - try IERC165(collateral_).supportsInterface(0x80ac58cd) returns (bool supportsERC721Interface) { - if (!supportsERC721Interface) revert NFTNotSupported(); - } catch { - revert NFTNotSupported(); - } + uint256 quoteTokenScale = 10**(18 - IERC20Token(quote_).decimals()); - nftType = NFTTypes.STANDARD_ERC721; + try IERC165(collateral_).supportsInterface(0x80ac58cd) returns (bool supportsERC721Interface) { + if (!supportsERC721Interface) revert NFTNotSupported(); + } catch { + revert NFTNotSupported(); } bytes memory data = abi.encodePacked( @@ -84,8 +70,7 @@ contract ERC721PoolFactory is PoolDeployer, IERC721PoolFactory { collateral_, quote_, quoteTokenScale, - tokenIds_.length, - nftType + tokenIds_.length ); ERC721Pool pool = ERC721Pool(address(implementation).clone(data)); @@ -93,7 +78,7 @@ contract ERC721PoolFactory is PoolDeployer, IERC721PoolFactory { pool_ = address(pool); // Track the newly deployed pool - deployedPools[getNFTSubsetHash(tokenIds_)][collateral_][quote_] = pool_; + deployedPools[subsetHash][collateral_][quote_] = pool_; deployedPoolsList.push(pool_); emit PoolCreated(pool_); @@ -105,8 +90,36 @@ contract ERC721PoolFactory is PoolDeployer, IERC721PoolFactory { /*** Pool Creation Functions ***/ /*******************************/ + /** + * @notice Get the hash of the subset of NFTs that will be used to create the pool + * @dev If no tokenIds are provided, the default ERC721_NON_SUBSET_HASH is returned + * @param tokenIds_ The array of token ids that will be used to create the pool + * @return bytes32 The hash of the subset of NFTs that will be used to create the pool + */ function getNFTSubsetHash(uint256[] memory tokenIds_) public pure returns (bytes32) { if (tokenIds_.length == 0) return ERC721_NON_SUBSET_HASH; - else return keccak256(abi.encodePacked(tokenIds_)); + else { + // check the array of token ids is sorted in ascending order + // revert if not sorted + _checkTokenIdSortOrder(tokenIds_); + + // hash the sorted array of tokenIds + return keccak256(abi.encode(tokenIds_)); + } } + + /** + * @notice Check that the array of token ids is sorted in ascending order, else revert. + * @dev The counters are modified in unchecked blocks due to being bounded by array length + * @param tokenIds_ The array of token ids to check for sorting + */ + function _checkTokenIdSortOrder(uint256[] memory tokenIds_) internal pure { + for (uint256 i = 0; i < tokenIds_.length - 1; ) { + if (tokenIds_[i] >= tokenIds_[i + 1]) revert TokenIdSubsetInvalid(); + unchecked { + ++i; + } + } + } + } diff --git a/src/PoolInfoUtils.sol b/src/PoolInfoUtils.sol index 807bf57e9..f395952d6 100644 --- a/src/PoolInfoUtils.sol +++ b/src/PoolInfoUtils.sol @@ -58,13 +58,13 @@ contract PoolInfoUtils { /** * @notice Get a bucket struct for a given index. - * @param index_ The index of the bucket to retrieve. - * @return price_ Bucket price (WAD) - * @return quoteTokens_ Amount of quote token in bucket, deposit + interest (WAD) - * @return collateral_ Unencumbered collateral in bucket (WAD). - * @return bucketLPs_ Outstanding LP balance in bucket (WAD) - * @return scale_ Lender interest multiplier (WAD). - * @return exchangeRate_ The exchange rate of the bucket, in RAY units. + * @param index_ The index of the bucket to retrieve. + * @return price_ Bucket price (WAD) + * @return quoteTokens_ Amount of quote token in bucket, deposit + interest (WAD) + * @return collateral_ Unencumbered collateral in bucket (WAD). + * @return bucketLPs_ Outstanding LP balance in bucket (WAD) + * @return scale_ Lender interest multiplier (WAD). + * @return exchangeRate_ The exchange rate of the bucket, in WAD units. */ function bucketInfo(address ajnaPool_, uint256 index_) external @@ -83,12 +83,7 @@ contract PoolInfoUtils { price_ = _priceAt(index_); (bucketLPs_, collateral_, , quoteTokens_, scale_) = pool.bucketInfo(index_); - if (bucketLPs_ == 0) { - exchangeRate_ = Maths.RAY; - } else { - uint256 bucketSize = quoteTokens_ * 1e18 + price_ * collateral_; // 10^36 + // 10^36 - exchangeRate_ = bucketSize * 1e18 / bucketLPs_; // 10^27 - } + exchangeRate_ = Buckets.getExchangeRate(collateral_, bucketLPs_, quoteTokens_, price_); } /** @@ -187,7 +182,7 @@ contract PoolInfoUtils { (,uint256 poolDebt,) = pool.debtInfo(); uint256 poolSize = pool.depositSize(); - uint256 quoteTokenBalance = IERC20Token(pool.quoteTokenAddress()).balanceOf(ajnaPool_); + uint256 quoteTokenBalance = IERC20Token(pool.quoteTokenAddress()).balanceOf(ajnaPool_) * pool.quoteTokenScale(); (uint256 bondEscrowed, uint256 unclaimedReserve, uint256 auctionKickTime) = pool.reservesInfo(); @@ -352,13 +347,13 @@ contract PoolInfoUtils { /** * @notice Calculate the amount of quote tokens in bucket for a given amount of LP Tokens. - * @param lpTokens_ The number of lpTokens to calculate amounts for. + * @param lps_ The number of LPs to calculate amounts for. * @param index_ The price bucket index for which the value should be calculated. * @return quoteAmount_ The exact amount of quote tokens that can be exchanged for the given LP Tokens, WAD units. */ function lpsToQuoteTokens( address ajnaPool_, - uint256 lpTokens_, + uint256 lps_, uint256 index_ ) external view returns (uint256 quoteAmount_) { IPool pool = IPool(ajnaPool_); @@ -367,7 +362,7 @@ contract PoolInfoUtils { bucketLPs_, bucketCollateral, bucketDeposit, - lpTokens_, + lps_, bucketDeposit, _priceAt(index_) ); @@ -375,13 +370,13 @@ contract PoolInfoUtils { /** * @notice Calculate the amount of collateral tokens in bucket for a given amount of LP Tokens. - * @param lpTokens_ The number of lpTokens to calculate amounts for. + * @param lps_ The number of LPs to calculate amounts for. * @param index_ The price bucket index for which the value should be calculated. * @return collateralAmount_ The exact amount of collateral tokens that can be exchanged for the given LP Tokens, WAD units. */ function lpsToCollateral( address ajnaPool_, - uint256 lpTokens_, + uint256 lps_, uint256 index_ ) external view returns (uint256 collateralAmount_) { IPool pool = IPool(ajnaPool_); @@ -390,7 +385,7 @@ contract PoolInfoUtils { bucketCollateral, bucketLPs_, bucketDeposit, - lpTokens_, + lps_, _priceAt(index_) ); } diff --git a/src/PositionManager.sol b/src/PositionManager.sol index f23de3393..d1caa295b 100644 --- a/src/PositionManager.sol +++ b/src/PositionManager.sol @@ -165,7 +165,7 @@ contract PositionManager is ERC721, PermitERC721, IPositionManager, Multicall, R emit MemorializePosition(owner, params_.tokenId); - // update pool lp token accounting and transfer ownership of lp tokens to PositionManager contract + // update pool lps accounting and transfer ownership of lps to PositionManager contract pool.transferLPs(owner, address(this), params_.indexes); } @@ -180,7 +180,7 @@ contract PositionManager is ERC721, PermitERC721, IPositionManager, Multicall, R */ function mint( MintParams calldata params_ - ) external override returns (uint256 tokenId_) { + ) external override nonReentrant returns (uint256 tokenId_) { tokenId_ = _nextId++; // revert if the address is not a valid Ajna pool @@ -252,7 +252,8 @@ contract PositionManager is ERC721, PermitERC721, IPositionManager, Multicall, R ) = IPool(params_.pool).moveQuoteToken( maxQuote, params_.fromIndex, - params_.toIndex + params_.toIndex, + params_.expiry ); // update position LPs state @@ -307,7 +308,7 @@ contract PositionManager is ERC721, PermitERC721, IPositionManager, Multicall, R emit RedeemPosition(owner, params_.tokenId); - // update pool lp token accounting and transfer ownership of lp tokens from PositionManager contract + // update pool lps accounting and transfer ownership of lps from PositionManager contract pool.transferLPs(address(this), owner, params_.indexes); } @@ -350,7 +351,7 @@ contract PositionManager is ERC721, PermitERC721, IPositionManager, Multicall, R /**********************/ /// @inheritdoc IPositionManagerDerivedState - function getLPTokens( + function getLPs( uint256 tokenId_, uint256 index_ ) external override view returns (uint256) { diff --git a/src/RewardsManager.sol b/src/RewardsManager.sol index 0e0ab2d38..bd406c5a6 100644 --- a/src/RewardsManager.sol +++ b/src/RewardsManager.sol @@ -107,11 +107,13 @@ contract RewardsManager is IRewardsManager { uint256 tokenId_, uint256 epochToClaim_ ) external override { - if (msg.sender != stakes[tokenId_].owner) revert NotOwnerOfDeposit(); + StakeInfo storage stakeInfo = stakes[tokenId_]; + + if (msg.sender != stakeInfo.owner) revert NotOwnerOfDeposit(); if (isEpochClaimed[tokenId_][epochToClaim_]) revert AlreadyClaimed(); - _claimRewards(tokenId_, epochToClaim_); + _claimRewards(stakeInfo, tokenId_, epochToClaim_, true, stakeInfo.ajnaPool); } /** @@ -149,8 +151,8 @@ contract RewardsManager is IRewardsManager { BucketState storage bucketState = stakeInfo.snapshot[bucketId]; - // record the number of lp tokens in bucket at the time of staking - bucketState.lpsAtStakeTime = positionManager.getLPTokens( + // record the number of lps in bucket at the time of staking + bucketState.lpsAtStakeTime = positionManager.getLPs( tokenId_, bucketId ); @@ -173,7 +175,7 @@ contract RewardsManager is IRewardsManager { ); // transfer rewards to sender - IERC20(ajnaToken).safeTransfer(msg.sender, updateReward); + _transferAjnaRewards(updateReward); } /** @@ -187,13 +189,30 @@ contract RewardsManager is IRewardsManager { function unstake( uint256 tokenId_ ) external override { - if (msg.sender != stakes[tokenId_].owner) revert NotOwnerOfDeposit(); + StakeInfo storage stakeInfo = stakes[tokenId_]; - address ajnaPool = stakes[tokenId_].ajnaPool; + if (msg.sender != stakeInfo.owner) revert NotOwnerOfDeposit(); + + address ajnaPool = stakeInfo.ajnaPool; // claim rewards, if any - _claimRewards(tokenId_, IPool(ajnaPool).currentBurnEpoch()); + _claimRewards( + stakeInfo, + tokenId_, + IPool(ajnaPool).currentBurnEpoch(), + false, + ajnaPool + ); + + // remove bucket snapshots recorded at the time of staking + uint256[] memory positionIndexes = positionManager.getPositionIndexes(tokenId_); + for (uint256 i = 0; i < positionIndexes.length; ) { + delete stakeInfo.snapshot[positionIndexes[i]]; // reset BucketState struct for current position + unchecked { ++i; } + } + + // remove recorded stake info delete stakes[tokenId_]; emit Unstake(msg.sender, ajnaPool, tokenId_); @@ -214,7 +233,7 @@ contract RewardsManager is IRewardsManager { updateReward = _updateBucketExchangeRates(pool_, indexes_); // transfer rewards to sender - IERC20(ajnaToken).safeTransfer(msg.sender, updateReward); + _transferAjnaRewards(updateReward); } /*******************************/ @@ -255,7 +274,19 @@ contract RewardsManager is IRewardsManager { return ( stakes[tokenId_].owner, stakes[tokenId_].ajnaPool, - stakes[tokenId_].lastInteractionBurnEpoch); + stakes[tokenId_].lastInteractionBurnEpoch + ); + } + + /// @inheritdoc IRewardsManagerState + function getBucketStateStakeInfo( + uint256 tokenId_, + uint256 bucketId_ + ) external view override returns (uint256, uint256) { + return ( + stakes[tokenId_].snapshot[bucketId_].lpsAtStakeTime, + stakes[tokenId_].snapshot[bucketId_].rateAtStakeTime + ); } /**************************/ @@ -291,15 +322,13 @@ contract RewardsManager is IRewardsManager { positionIndexes ); - uint256 nextEpoch = epoch + 1; - - // update epoch token claim trackers - rewardsClaimed[nextEpoch] += nextEpochRewards; - isEpochClaimed[tokenId_][nextEpoch] = true; - rewards_ += nextEpochRewards; unchecked { ++epoch; } + + // update epoch token claim trackers + rewardsClaimed[epoch] += nextEpochRewards; + isEpochClaimed[tokenId_][epoch] = true; } } @@ -391,7 +420,7 @@ contract RewardsManager is IRewardsManager { // calculate the equivalent amount of quote tokens given the stakes lp balance, // and the exchange rate at the next and current burn events - interestEarned_ = Maths.rayToWad(Maths.rmul(nextExchangeRate - exchangeRate_, bucketLPs)); + interestEarned_ = Maths.wmul(nextExchangeRate - exchangeRate_, bucketLPs); } } @@ -423,8 +452,9 @@ contract RewardsManager is IRewardsManager { // calculate rewards earned newRewards_ = Maths.wmul( REWARD_FACTOR, - Maths.wmul( - Maths.wdiv(interestEarned_, totalInterestEarnedInPeriod), totalBurnedInPeriod + Maths.wdiv( + Maths.wmul(interestEarned_, totalBurnedInPeriod), + totalInterestEarnedInPeriod ) ); @@ -440,47 +470,49 @@ contract RewardsManager is IRewardsManager { /** * @notice Claim rewards that have been accumulated by a staked NFT. - * @param tokenId_ ID of the staked LP NFT. - * @param epochToClaim_ The burn epoch to claim rewards for (rewards calculation starts from the last claimed epoch) + * @param stakeInfo_ Details of stake to claim rewards for. + * @param tokenId_ ID of the staked LP NFT. + * @param epochToClaim_ The burn epoch to claim rewards for (rewards calculation starts from the last claimed epoch) + * @param validateEpoch_ True if the epoch is received as a parameter and needs to be validated (lower or equal with latest epoch). + * @param ajnaPool_ Address of ajna pool associated with the stake. */ function _claimRewards( + StakeInfo storage stakeInfo_, uint256 tokenId_, - uint256 epochToClaim_ + uint256 epochToClaim_, + bool validateEpoch_, + address ajnaPool_ ) internal { - StakeInfo storage stakeInfo = stakes[tokenId_]; - address ajnaPool = stakeInfo.ajnaPool; + // revert if higher epoch to claim than current burn epoch + if (validateEpoch_ && epochToClaim_ > IPool(ajnaPool_).currentBurnEpoch()) revert EpochNotAvailable(); // update bucket exchange rates and claim associated rewards uint256 rewardsEarned = _updateBucketExchangeRates( - ajnaPool, + ajnaPool_, positionManager.getPositionIndexes(tokenId_) ); rewardsEarned += _calculateAndClaimRewards(tokenId_, epochToClaim_); uint256[] memory burnEpochsClaimed = _getBurnEpochsClaimed( - stakeInfo.lastInteractionBurnEpoch, + stakeInfo_.lastInteractionBurnEpoch, epochToClaim_ ); emit ClaimRewards( msg.sender, - ajnaPool, + ajnaPool_, tokenId_, burnEpochsClaimed, rewardsEarned ); // update last interaction burn event - stakeInfo.lastInteractionBurnEpoch = uint96(epochToClaim_); - - uint256 ajnaBalance = IERC20(ajnaToken).balanceOf(address(this)); - - if (rewardsEarned > ajnaBalance) rewardsEarned = ajnaBalance; + stakeInfo_.lastInteractionBurnEpoch = uint96(epochToClaim_); // transfer rewards to sender - IERC20(ajnaToken).safeTransfer(msg.sender, rewardsEarned); + _transferAjnaRewards(rewardsEarned); } /** @@ -688,6 +720,20 @@ contract RewardsManager is IRewardsManager { } } + /** @notice Utility method to transfer Ajna rewards to the sender + * @dev This method is used to transfer rewards to the sender after a successful claim or update. + * @dev It is used to ensure that rewards claimers will be able to claim some portion of the remaining tokens if a claim would exceed the remaining contract balance. + * @param rewardsEarned_ Amount of rewards earned by the caller. + */ + function _transferAjnaRewards(uint256 rewardsEarned_) internal { + // check that rewards earned isn't greater than remaining balance + // if remaining balance is greater, set to remaining balance + uint256 ajnaBalance = IERC20(ajnaToken).balanceOf(address(this)); + if (rewardsEarned_ > ajnaBalance) rewardsEarned_ = ajnaBalance; + // transfer rewards to sender + IERC20(ajnaToken).safeTransfer(msg.sender, rewardsEarned_); + } + /************************/ /*** Helper Functions ***/ /************************/ diff --git a/src/base/FlashloanablePool.sol b/src/base/FlashloanablePool.sol index 1beba296d..22e70da69 100644 --- a/src/base/FlashloanablePool.sol +++ b/src/base/FlashloanablePool.sol @@ -2,6 +2,9 @@ pragma solidity 0.8.14; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + import { Pool } from './Pool.sol'; import { IERC3156FlashBorrower } from '../interfaces/pool/IERC3156FlashBorrower.sol'; @@ -12,36 +15,45 @@ import { IERC3156FlashBorrower } from '../interfaces/pool/IERC3156FlashBorrower. * @notice Flash loans can be taking in ERC20 quote and ERC20 collateral tokens. */ abstract contract FlashloanablePool is Pool { + using SafeERC20 for IERC20; + /** * @notice Called by flashloan borrowers to borrow liquidity which must be repaid in the same transaction. * @param receiver_ Address of the contract which implements the appropriate interface to receive tokens. * @param token_ Address of the ERC20 token caller wants to borrow. * @param amount_ The amount of tokens to borrow. * @param data_ User-defined calldata passed to the receiver. - * @return True if successful. + * @return success_ True if flashloan was successful. */ function flashLoan( IERC3156FlashBorrower receiver_, address token_, uint256 amount_, bytes calldata data_ - ) external virtual override nonReentrant returns (bool) { - if (token_ == _getArgAddress(QUOTE_ADDRESS)) return _flashLoanQuoteToken(receiver_, token_, amount_, data_); - revert FlashloanUnavailableForToken(); - } + ) external virtual override nonReentrant returns (bool success_) { + if (!_isFlashloanSupported(token_)) revert FlashloanUnavailableForToken(); + + IERC20 tokenContract = IERC20(token_); + + uint256 initialBalance = tokenContract.balanceOf(address(this)); + + tokenContract.safeTransfer( + address(receiver_), + amount_ + ); - function _flashLoanQuoteToken(IERC3156FlashBorrower receiver_, - address token_, - uint256 amount_, - bytes calldata data_ - ) internal returns (bool) { - _transferQuoteToken(address(receiver_), amount_); - if (receiver_.onFlashLoan(msg.sender, token_, amount_, 0, data_) != keccak256("ERC3156FlashBorrower.onFlashLoan")) revert FlashloanCallbackFailed(); - _transferQuoteTokenFrom(address(receiver_), amount_); - return true; + tokenContract.safeTransferFrom( + address(receiver_), + address(this), + amount_ + ); + + if (tokenContract.balanceOf(address(this)) != initialBalance) revert FlashloanIncorrectBalance(); + + success_ = true; } /** @@ -51,7 +63,7 @@ abstract contract FlashloanablePool is Pool { address token_, uint256 ) external virtual view override returns (uint256) { - if (token_ != _getArgAddress(QUOTE_ADDRESS)) revert FlashloanUnavailableForToken(); + if (!_isFlashloanSupported(token_)) revert FlashloanUnavailableForToken(); return 0; } @@ -63,6 +75,18 @@ abstract contract FlashloanablePool is Pool { function maxFlashLoan( address token_ ) external virtual view override returns (uint256 maxLoan_) { - if (token_ == _getArgAddress(QUOTE_ADDRESS)) maxLoan_ = _getPoolQuoteTokenBalance(); + if (_isFlashloanSupported(token_)) maxLoan_ = IERC20(token_).balanceOf(address(this)); + } + + /** + * @notice Returns true if pool allows flashloans for given token address, false otherwise. + * @dev Allows flashloans for quote token, overriden in pool implementation to allow flashloans for other tokens. + * @param token_ Address of the ERC20 token to be lent. + * @return True if token can be flashloaned, false otherwise. + */ + function _isFlashloanSupported( + address token_ + ) internal virtual view returns (bool) { + return token_ == _getArgAddress(QUOTE_ADDRESS); } } \ No newline at end of file diff --git a/src/base/PermitERC721.sol b/src/base/PermitERC721.sol index a8f3a87af..41084f410 100644 --- a/src/base/PermitERC721.sol +++ b/src/base/PermitERC721.sol @@ -109,7 +109,6 @@ abstract contract PermitERC721 is ERC721, IPermit { * @notice Called by an NFT owner to enable their NFT to be transferred by a spender address without making a seperate approve call * @param from_ The address of the current owner of the NFT * @param to_ The address of the new owner of the NFT - * @param spender_ The address of the third party who will execute the transaction involving an owners NFT * @param tokenId_ The id of the NFT being interacted with * @param deadline_ The unix timestamp by which the permit must be called * @param v_ Component of secp256k1 signature @@ -117,9 +116,9 @@ abstract contract PermitERC721 is ERC721, IPermit { * @param s_ Component of secp256k1 signature */ function safeTransferFromWithPermit( - address from_, address to_, address spender_, uint256 tokenId_, uint256 deadline_, uint8 v_, bytes32 r_, bytes32 s_ + address from_, address to_, uint256 tokenId_, uint256 deadline_, uint8 v_, bytes32 r_, bytes32 s_ ) external { - this.permit(spender_, tokenId_, deadline_, v_, r_, s_); + this.permit(msg.sender, tokenId_, deadline_, v_, r_, s_); safeTransferFrom(from_, to_, tokenId_); } diff --git a/src/base/Pool.sol b/src/base/Pool.sol index f51c77f5f..36a7a7e84 100644 --- a/src/base/Pool.sol +++ b/src/base/Pool.sol @@ -45,7 +45,8 @@ import { } from '../libraries/helpers/PoolHelper.sol'; import { _revertIfAuctionDebtLocked, - _revertIfAuctionClearable + _revertIfAuctionClearable, + _revertOnExpiry } from '../libraries/helpers/RevertsHelper.sol'; import { Buckets } from '../libraries/internal/Buckets.sol'; @@ -93,7 +94,7 @@ abstract contract Pool is Clone, ReentrancyGuard, Multicall, IPool { bool internal isPoolInitialized; - mapping(address => mapping(address => mapping(uint256 => uint256))) private _lpTokenAllowances; // owner address -> new owner address -> deposit index -> allowed amount + mapping(address => mapping(address => mapping(uint256 => uint256))) private _lpAllowances; // owner address -> new owner address -> deposit index -> allowed amount /******************/ /*** Immutables ***/ @@ -131,8 +132,10 @@ abstract contract Pool is Clone, ReentrancyGuard, Multicall, IPool { /// @inheritdoc IPoolLenderActions function addQuoteToken( uint256 quoteTokenAmountToAdd_, - uint256 index_ + uint256 index_, + uint256 expiry_ ) external override nonReentrant returns (uint256 bucketLPs_) { + _revertOnExpiry(expiry_); PoolState memory poolState = _accruePoolInterest(); // round to token precision @@ -159,22 +162,24 @@ abstract contract Pool is Clone, ReentrancyGuard, Multicall, IPool { /** * @inheritdoc IPoolLenderActions * @dev write state: - * - _lpTokenAllowances mapping + * - _lpAllowances mapping */ function approveLpOwnership( - address allowedNewOwner_, + address newOwner, uint256 index_, - uint256 lpsAmountToApprove_ + uint256 amount_ ) external nonReentrant { - _lpTokenAllowances[msg.sender][allowedNewOwner_][index_] = lpsAmountToApprove_; + _lpAllowances[msg.sender][newOwner][index_] = amount_; } /// @inheritdoc IPoolLenderActions function moveQuoteToken( uint256 maxAmountToMove_, uint256 fromIndex_, - uint256 toIndex_ + uint256 toIndex_, + uint256 expiry_ ) external override nonReentrant returns (uint256 fromBucketLPs_, uint256 toBucketLPs_) { + _revertOnExpiry(expiry_); PoolState memory poolState = _accruePoolInterest(); _revertIfAuctionDebtLocked(deposits, poolBalances, fromIndex_, poolState.inflator); @@ -242,7 +247,7 @@ abstract contract Pool is Clone, ReentrancyGuard, Multicall, IPool { ) external override nonReentrant { LenderActions.transferLPs( buckets, - _lpTokenAllowances, + _lpAllowances, owner_, newOwner_, indexes_ @@ -273,11 +278,11 @@ abstract contract Pool is Clone, ReentrancyGuard, Multicall, IPool { ); // update pool balances state + poolBalances.t0Debt = result.t0PoolDebt; poolBalances.t0DebtInAuction += result.t0KickedDebt; - poolBalances.t0Debt += result.t0KickPenalty; // update pool interest rate state - poolState.debt += result.kickPenalty; + poolState.debt = Maths.wmul(result.t0PoolDebt, poolState.inflator); _updateInterestState(poolState, result.lup); if(result.amountToCoverBond != 0) _transferQuoteTokenFrom(msg.sender, result.amountToCoverBond); @@ -304,11 +309,11 @@ abstract contract Pool is Clone, ReentrancyGuard, Multicall, IPool { ); // update pool balances state - poolBalances.t0Debt += result.t0KickPenalty; + poolBalances.t0Debt = result.t0PoolDebt; poolBalances.t0DebtInAuction += result.t0KickedDebt; // update pool interest rate state - poolState.debt += result.kickPenalty; + poolState.debt = Maths.wmul(result.t0PoolDebt, poolState.inflator); _updateInterestState(poolState, result.lup); // transfer from kicker to pool the difference to cover bond @@ -320,10 +325,10 @@ abstract contract Pool is Clone, ReentrancyGuard, Multicall, IPool { * @dev write state: * - reset kicker's claimable accumulator */ - function withdrawBonds() external { + function withdrawBonds(address recipient_) external { uint256 claimable = auctions.kickers[msg.sender].claimable; auctions.kickers[msg.sender].claimable = 0; - _transferQuoteToken(msg.sender, claimable); + _transferQuoteToken(recipient_, claimable); } /*********************************/ @@ -356,8 +361,8 @@ abstract contract Pool is Clone, ReentrancyGuard, Multicall, IPool { reserveAuction, StartReserveAuctionParams({ poolSize: Deposits.treeSum(deposits), - poolDebt: poolBalances.t0Debt, - poolBalance: _getPoolQuoteTokenBalance(), + t0PoolDebt: poolBalances.t0Debt, + poolBalance: _getNormalizedPoolQuoteTokenBalance(), inflator: inflatorState.inflator }) ); @@ -424,10 +429,7 @@ abstract contract Pool is Clone, ReentrancyGuard, Multicall, IPool { * @return poolState_ Struct containing pool details. */ function _accruePoolInterest() internal returns (PoolState memory poolState_) { - // retrieve t0Debt amount from poolBalances struct - uint256 t0Debt = poolBalances.t0Debt; - - // initialize fields of poolState_ struct with initial values + poolState_.t0Debt = poolBalances.t0Debt; poolState_.collateral = poolBalances.pledgedCollateral; poolState_.inflator = inflatorState.inflator; poolState_.rate = interestState.interestRate; @@ -435,9 +437,9 @@ abstract contract Pool is Clone, ReentrancyGuard, Multicall, IPool { poolState_.quoteDustLimit = _getArgUint256(QUOTE_SCALE); // check if t0Debt is not equal to 0, indicating that there is debt to be tracked for the pool - if (t0Debt != 0) { + if (poolState_.t0Debt != 0) { // Calculate prior pool debt - poolState_.debt = Maths.wmul(t0Debt, poolState_.inflator); + poolState_.debt = Maths.wmul(poolState_.t0Debt, poolState_.inflator); // calculate elapsed time since inflator was last updated uint256 elapsed = block.timestamp - inflatorState.inflatorUpdate; @@ -455,7 +457,7 @@ abstract contract Pool is Clone, ReentrancyGuard, Multicall, IPool { ); poolState_.inflator = newInflator; // After debt owed to lenders has accrued, calculate current debt owed by borrowers - poolState_.debt = Maths.wmul(t0Debt, poolState_.inflator); + poolState_.debt = Maths.wmul(poolState_.t0Debt, poolState_.inflator); // update total interest earned accumulator with the newly accrued interest reserveAuction.totalInterestEarned += newInterest; @@ -507,8 +509,11 @@ abstract contract Pool is Clone, ReentrancyGuard, Multicall, IPool { IERC20(_getArgAddress(QUOTE_ADDRESS)).safeTransfer(to_, amount_ / _getArgUint256(QUOTE_SCALE)); } - function _getPoolQuoteTokenBalance() internal view returns (uint256) { - return IERC20(_getArgAddress(QUOTE_ADDRESS)).balanceOf(address(this)); + /** + * @dev returns the pool quote token balance normalized to WAD to be used for calculating pool reserves + */ + function _getNormalizedPoolQuoteTokenBalance() internal view returns (uint256) { + return IERC20(_getArgAddress(QUOTE_ADDRESS)).balanceOf(address(this)) * _getArgUint256(QUOTE_SCALE); } function _lup(uint256 debt_) internal view returns (uint256) { @@ -710,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/base/PoolDeployer.sol b/src/base/PoolDeployer.sol index ffb433a4b..2ae7459c6 100644 --- a/src/base/PoolDeployer.sol +++ b/src/base/PoolDeployer.sol @@ -35,10 +35,9 @@ abstract contract PoolDeployer { * @notice Ensures that pools are deployed according to specifications. * @dev Used by both ERC20, and ERC721 pool factory types. */ - modifier canDeploy(bytes32 subsetHash_, address collateral_, address quote_, uint256 interestRate_) { + modifier canDeploy(address collateral_, address quote_, uint256 interestRate_) { if (collateral_ == address(0) || quote_ == address(0)) revert IPoolFactory.DeployWithZeroAddress(); - if (deployedPools[subsetHash_][collateral_][quote_] != address(0)) revert IPoolFactory.PoolAlreadyExists(); - if (MIN_RATE >= interestRate_ || interestRate_ >= MAX_RATE) revert IPoolFactory.PoolInterestRateInvalid(); + if (MIN_RATE > interestRate_ || interestRate_ > MAX_RATE) revert IPoolFactory.PoolInterestRateInvalid(); _; } diff --git a/src/interfaces/pool/IPool.sol b/src/interfaces/pool/IPool.sol index 2a203fbe9..8ced68587 100644 --- a/src/interfaces/pool/IPool.sol +++ b/src/interfaces/pool/IPool.sol @@ -44,7 +44,7 @@ interface IERC20Token { } interface IERC721Token { - function safeTransferFrom( + function transferFrom( address from, address to, uint256 tokenId diff --git a/src/interfaces/pool/commons/IPoolErrors.sol b/src/interfaces/pool/commons/IPoolErrors.sol index 7641ee9d3..a0803f5f3 100644 --- a/src/interfaces/pool/commons/IPoolErrors.sol +++ b/src/interfaces/pool/commons/IPoolErrors.sol @@ -75,6 +75,11 @@ interface IPoolErrors { */ error FlashloanCallbackFailed(); + /** + * @notice Balance of pool contract before flash loan is different than the balance after flash loan. + */ + error FlashloanIncorrectBalance(); + /** * @notice Pool cannot facilitate a flashloan for the specified token address. */ @@ -105,7 +110,7 @@ interface IPoolErrors { /** * @notice Borrower is attempting to borrow more quote token than is available before the supplied limitIndex. */ - error LimitIndexReached(); + error LimitIndexExceeded(); /** * @notice When moving quote token HTP must stay below LUP. @@ -179,6 +184,16 @@ interface IPoolErrors { */ error TakeNotPastCooldown(); + /** + * @notice Current block timestamp has reached or exceeded a user-provided expiration. + */ + error TransactionExpired(); + + /** + * @notice Owner of the LP tokens attemps to transfer LPs to same address. + */ + error TransferToSameOwner(); + /** * @notice The threshold price of the loan to be inserted in loans heap is zero. */ diff --git a/src/interfaces/pool/commons/IPoolEvents.sol b/src/interfaces/pool/commons/IPoolEvents.sol index f8c7cbf47..49927d9b6 100644 --- a/src/interfaces/pool/commons/IPoolEvents.sol +++ b/src/interfaces/pool/commons/IPoolEvents.sol @@ -211,13 +211,13 @@ interface IPoolEvents { * @param owner The original owner address of the position. * @param newOwner The new owner address of the position. * @param indexes Array of price bucket indexes at which LP tokens were transferred. - * @param lpTokens Amount of LP tokens transferred. + * @param lps Amount of LPs transferred. */ - event TransferLPTokens( + event TransferLPs( address owner, address newOwner, uint256[] indexes, - uint256 lpTokens + uint256 lps ); /** diff --git a/src/interfaces/pool/commons/IPoolInternals.sol b/src/interfaces/pool/commons/IPoolInternals.sol index 197e4ecb2..576f3a721 100644 --- a/src/interfaces/pool/commons/IPoolInternals.sol +++ b/src/interfaces/pool/commons/IPoolInternals.sol @@ -11,22 +11,22 @@ pragma solidity 0.8.14; /*****************************/ struct BucketTakeResult { - uint256 collateralAmount; - uint256 t0RepayAmount; - uint256 t0DebtPenalty; - uint256 remainingCollateral; - uint256 poolDebt; - uint256 newLup; - uint256 t0DebtInAuctionChange; - bool settledAuction; + uint256 collateralAmount; // [WAD] amount of collateral taken + uint256 compensatedCollateral; // [WAD] amount of borrower collateral that is compensated with LPs + uint256 t0DebtPenalty; // [WAD] t0 penalty applied on first take + uint256 remainingCollateral; // [WAD] amount of borrower collateral remaining after take + uint256 poolDebt; // [WAD] current pool debt + uint256 t0PoolDebt; // [WAD] t0 pool debt + uint256 newLup; // [WAD] current lup + uint256 t0DebtInAuctionChange; // [WAD] the amount of t0 debt recovered by take action + bool settledAuction; // true if auction is settled by take action } struct KickResult { - uint256 amountToCoverBond; // amount of bond that needs to be covered - uint256 kickPenalty; // kick penalty - uint256 t0KickPenalty; // t0 kick penalty - uint256 t0KickedDebt; // new t0 debt after kick - uint256 lup; // current lup + uint256 amountToCoverBond; // [WAD] amount of bond that needs to be covered + uint256 t0PoolDebt; // [WAD] t0 debt in pool after kick + uint256 t0KickedDebt; // [WAD] new t0 debt after kick + uint256 lup; // [WAD] current lup } struct SettleParams { @@ -38,16 +38,17 @@ struct SettleParams { } struct TakeResult { - uint256 collateralAmount; - uint256 quoteTokenAmount; - uint256 t0RepayAmount; - uint256 t0DebtPenalty; - uint256 excessQuoteToken; - uint256 remainingCollateral; - uint256 poolDebt; - uint256 newLup; - uint256 t0DebtInAuctionChange; - bool settledAuction; + uint256 collateralAmount; // [WAD] amount of collateral taken + uint256 compensatedCollateral; // [WAD] amount of borrower collateral that is compensated with LPs + uint256 quoteTokenAmount; // [WAD] amount of quote tokens paid by taker for taken collateral + uint256 t0DebtPenalty; // [WAD] t0 penalty applied on first take + uint256 excessQuoteToken; // [WAD] (NFT only) amount of quote tokens to be paid by taker to borrower for fractional collateral + uint256 remainingCollateral; // [WAD] amount of borrower collateral remaining after take + uint256 poolDebt; // [WAD] current pool debt + uint256 t0PoolDebt; // [WAD] t0 pool debt + uint256 newLup; // [WAD] current lup + uint256 t0DebtInAuctionChange; // [WAD] the amount of t0 debt recovered by take action + bool settledAuction; // true if auction is settled by take action } /******************************************/ @@ -83,7 +84,7 @@ struct DrawDebtResult { uint256 remainingCollateral; // [WAD] amount of borrower collateral after draw debt (for NFT can be diminished if auction settled) bool settledAuction; // true if collateral pledged settles auction uint256 t0DebtInAuctionChange; // [WAD] change of t0 pool debt in auction after pledge collateral - uint256 t0DebtChange; // [WAD] change of total t0 pool debt after after draw debt + uint256 t0PoolDebt; // [WAD] amount of t0 debt in pool after draw debt } struct RepayDebtResult { @@ -93,6 +94,6 @@ struct RepayDebtResult { uint256 remainingCollateral; // [WAD] amount of borrower collateral after pull collateral bool settledAuction; // true if repay debt settles auction uint256 t0DebtInAuctionChange; // [WAD] change of t0 pool debt in auction after repay debt - uint256 t0RepaidDebt; // [WAD] amount of t0 repaid debt + uint256 t0PoolDebt; // [WAD] amount of t0 debt in pool after repay uint256 quoteTokenToRepay; // [WAD] quote token amount to be transferred from sender to pool } \ No newline at end of file diff --git a/src/interfaces/pool/commons/IPoolLenderActions.sol b/src/interfaces/pool/commons/IPoolLenderActions.sol index 11546147f..d29172751 100644 --- a/src/interfaces/pool/commons/IPoolLenderActions.sol +++ b/src/interfaces/pool/commons/IPoolLenderActions.sol @@ -10,11 +10,13 @@ interface IPoolLenderActions { * @notice Called by lenders to add an amount of credit at a specified price bucket. * @param amount The amount of quote token to be added by a lender. * @param index The index of the bucket to which the quote tokens will be added. + * @param expiry Timestamp after which this TX will revert, preventing inclusion in a block with unfavorable price. * @return lpbChange The amount of LP Tokens changed for the added quote tokens. */ function addQuoteToken( uint256 amount, - uint256 index + uint256 index, + uint256 expiry ) external returns (uint256 lpbChange); /** @@ -35,13 +37,15 @@ interface IPoolLenderActions { * @param maxAmount The maximum amount of quote token to be moved by a lender. * @param fromIndex The bucket index from which the quote tokens will be removed. * @param toIndex The bucket index to which the quote tokens will be added. + * @param expiry Timestamp after which this TX will revert, preventing inclusion in a block with unfavorable price. * @return lpbAmountFrom The amount of LPs moved out from bucket. * @return lpbAmountTo The amount of LPs moved to destination bucket. */ function moveQuoteToken( uint256 maxAmount, uint256 fromIndex, - uint256 toIndex + uint256 toIndex, + uint256 expiry ) external returns (uint256 lpbAmountFrom, uint256 lpbAmountTo); /** diff --git a/src/interfaces/pool/commons/IPoolLiquidationActions.sol b/src/interfaces/pool/commons/IPoolLiquidationActions.sol index 150738f95..8f20c9047 100644 --- a/src/interfaces/pool/commons/IPoolLiquidationActions.sol +++ b/src/interfaces/pool/commons/IPoolLiquidationActions.sol @@ -63,6 +63,7 @@ interface IPoolLiquidationActions { /** * @notice Called by kickers to withdraw their auction bonds (the amount of quote tokens that are not locked in active auctions). + * @param recipient Address to receive claimed bonds amount. */ - function withdrawBonds() external; + function withdrawBonds(address recipient) external; } \ No newline at end of file diff --git a/src/interfaces/pool/commons/IPoolReserveAuctionActions.sol b/src/interfaces/pool/commons/IPoolReserveAuctionActions.sol index 9e28f5fbe..6ceed46dc 100644 --- a/src/interfaces/pool/commons/IPoolReserveAuctionActions.sol +++ b/src/interfaces/pool/commons/IPoolReserveAuctionActions.sol @@ -27,7 +27,7 @@ interface IPoolReserveAuctionActions { struct StartReserveAuctionParams { uint256 poolSize; // [WAD] total deposits in pool (with accrued debt) - uint256 poolDebt; // [WAD] current t0 pool debt + uint256 t0PoolDebt; // [WAD] current t0 pool debt uint256 poolBalance; // [WAD] pool quote token balance uint256 inflator; // [WAD] pool current inflator } \ No newline at end of file diff --git a/src/interfaces/pool/commons/IPoolState.sol b/src/interfaces/pool/commons/IPoolState.sol index d8123d53a..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); + } /*********************/ @@ -250,6 +270,7 @@ struct PoolBalancesState { struct PoolState { uint8 poolType; // pool type, can be ERC20 or ERC721 + uint256 t0Debt; // [WAD] t0 debt in pool uint256 debt; // [WAD] total debt in pool, accrued in current block uint256 collateral; // [WAD] total collateral pledged in pool uint256 inflator; // [WAD] current pool inflator @@ -261,12 +282,12 @@ struct PoolState { /*** Buckets State ***/ struct Lender { - uint256 lps; // [RAY] Lender LP accumulator + uint256 lps; // [WAD] Lender LP accumulator uint256 depositTime; // timestamp of last deposit } struct Bucket { - uint256 lps; // [RAY] Bucket LP accumulator + uint256 lps; // [WAD] Bucket LP accumulator uint256 collateral; // [WAD] Available collateral tokens deposited in the bucket uint256 bankruptcyTime; // Timestamp when bucket become insolvent, 0 if healthy mapping(address => Lender) lenders; // lender address to Lender struct mapping diff --git a/src/interfaces/pool/erc20/IERC20PoolBorrowerActions.sol b/src/interfaces/pool/erc20/IERC20PoolBorrowerActions.sol index dbab099da..d92a95712 100644 --- a/src/interfaces/pool/erc20/IERC20PoolBorrowerActions.sol +++ b/src/interfaces/pool/erc20/IERC20PoolBorrowerActions.sol @@ -29,10 +29,14 @@ interface IERC20PoolBorrowerActions { * @param borrowerAddress_ The borrower whose loan is being interacted with. * @param maxQuoteTokenAmountToRepay_ The amount of quote tokens to repay. * @param collateralAmountToPull_ The amount of collateral to be puled from the pool. + * @param recipient_ The address to receive amount of pulled collateral. + * @param limitIndex_ Ensures LUP has not moved far from state when borrower pulls collateral. */ function repayDebt( address borrowerAddress_, uint256 maxQuoteTokenAmountToRepay_, - uint256 collateralAmountToPull_ + uint256 collateralAmountToPull_, + address recipient_, + uint256 limitIndex_ ) external; } diff --git a/src/interfaces/pool/erc20/IERC20PoolLenderActions.sol b/src/interfaces/pool/erc20/IERC20PoolLenderActions.sol index b43ee2652..ce0831efb 100644 --- a/src/interfaces/pool/erc20/IERC20PoolLenderActions.sol +++ b/src/interfaces/pool/erc20/IERC20PoolLenderActions.sol @@ -9,11 +9,14 @@ interface IERC20PoolLenderActions { /** * @notice Deposit claimable collateral into a specified bucket. - * @param amount Amount of collateral to deposit. - * @param index The bucket index to which collateral will be deposited. + * @param amount Amount of collateral to deposit. + * @param index The bucket index to which collateral will be deposited. + * @param expiry Timestamp after which this TX will revert, preventing inclusion in a block with unfavorable price. + * @return lpbChange The amount of LP Tokens changed for the added collateral. */ function addCollateral( uint256 amount, - uint256 index + uint256 index, + uint256 expiry ) external returns (uint256 lpbChange); } \ No newline at end of file diff --git a/src/interfaces/pool/erc721/IERC721NonStandard.sol b/src/interfaces/pool/erc721/IERC721NonStandard.sol deleted file mode 100644 index eb66243bf..000000000 --- a/src/interfaces/pool/erc721/IERC721NonStandard.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.14; - -interface ICryptoKitties { - function transferFrom(address from_, address to_, uint256 tokenId_) external; - function transfer(address to_, uint256 tokenId_) external; - function approve(address to_, uint256 tokenId_) external; - function ownerOf(uint256 tokenId_) external returns(address); - function kittyIndexToApproved(uint256 tokenId_) external returns(address); -} - -interface ICryptoPunks { - function buyPunk(uint punkIndex) external; - function transferPunk(address to, uint punkIndex) external; - function offerPunkForSaleToAddress(uint punkIndex, uint minSalePriceInWei, address toAddress) external; - function punkIndexToAddress(uint punkIndex) external returns(address); -} - -enum NFTTypes{ STANDARD_ERC721, CRYPTOPUNKS, CRYPTOKITTIES } \ No newline at end of file diff --git a/src/interfaces/pool/erc721/IERC721PoolBorrowerActions.sol b/src/interfaces/pool/erc721/IERC721PoolBorrowerActions.sol index 5f8e36c95..9206c9b5e 100644 --- a/src/interfaces/pool/erc721/IERC721PoolBorrowerActions.sol +++ b/src/interfaces/pool/erc721/IERC721PoolBorrowerActions.sol @@ -29,10 +29,14 @@ interface IERC721PoolBorrowerActions { * @param borrowerAddress_ The borrower whose loan is being interacted with. * @param maxQuoteTokenAmountToRepay_ The amount of quote tokens to repay. * @param noOfNFTsToPull_ The integer number of NFT collateral to be puled from the pool. + * @param recipient_ The address to receive amount of pulled collateral. + * @param limitIndex_ Ensures LUP has not moved far from state when borrower pulls collateral. */ function repayDebt( address borrowerAddress_, uint256 maxQuoteTokenAmountToRepay_, - uint256 noOfNFTsToPull_ + uint256 noOfNFTsToPull_, + address recipient_, + uint256 limitIndex_ ) external; } diff --git a/src/interfaces/pool/erc721/IERC721PoolFactory.sol b/src/interfaces/pool/erc721/IERC721PoolFactory.sol index 52bfde301..30d842cbf 100644 --- a/src/interfaces/pool/erc721/IERC721PoolFactory.sol +++ b/src/interfaces/pool/erc721/IERC721PoolFactory.sol @@ -10,6 +10,15 @@ import { IPoolFactory } from '../IPoolFactory.sol'; */ interface IERC721PoolFactory is IPoolFactory { + /**************/ + /*** Errors ***/ + /**************/ + + /** + * @notice User tried to deploy a pool with an array of tokenIds that weren't sorted, or contained duplicates. + */ + error TokenIdSubsetInvalid(); + /**************************/ /*** External Functions ***/ /**************************/ diff --git a/src/interfaces/pool/erc721/IERC721PoolLenderActions.sol b/src/interfaces/pool/erc721/IERC721PoolLenderActions.sol index 1c07f06ec..cf068b951 100644 --- a/src/interfaces/pool/erc721/IERC721PoolLenderActions.sol +++ b/src/interfaces/pool/erc721/IERC721PoolLenderActions.sol @@ -11,10 +11,13 @@ interface IERC721PoolLenderActions { * @notice Deposit claimable collateral into a specified bucket. * @param tokenIds Array of collateral to deposit. * @param index The bucket index to which collateral will be deposited. + * @param expiry Timestamp after which this TX will revert, preventing inclusion in a block with unfavorable price. + * @return lpbChange The amount of LP Tokens changed for the added collateral. */ function addCollateral( uint256[] calldata tokenIds, - uint256 index + uint256 index, + uint256 expiry ) external returns (uint256); /** diff --git a/src/interfaces/position/IPositionManagerDerivedState.sol b/src/interfaces/position/IPositionManagerDerivedState.sol index a7e7614a2..a2123b174 100644 --- a/src/interfaces/position/IPositionManagerDerivedState.sol +++ b/src/interfaces/position/IPositionManagerDerivedState.sol @@ -8,16 +8,16 @@ pragma solidity 0.8.14; interface IPositionManagerDerivedState { /** - * @notice Returns the lpTokens accrued to a given tokenId, bucket pairing. + * @notice Returns the LPs accrued to a given tokenId, bucket pairing. * @dev Nested mappings aren't returned normally as part of the default getter for a mapping. - * @param tokenId Unique ID of token. - * @param index Index of bucket to check LP balance of. - * @return lpTokens Balance of lpTokens in the bucket for this position. + * @param tokenId Unique ID of token. + * @param index Index of bucket to check LP balance of. + * @return lps Balance of lps in the bucket for this position. */ - function getLPTokens( + function getLPs( uint256 tokenId, uint256 index - ) external view returns (uint256 lpTokens); + ) external view returns (uint256 lps); /** * @notice Returns an array of bucket indexes in which an NFT has liquidity. diff --git a/src/interfaces/position/IPositionManagerOwnerActions.sol b/src/interfaces/position/IPositionManagerOwnerActions.sol index 70b3ee84d..60e7e6110 100644 --- a/src/interfaces/position/IPositionManagerOwnerActions.sol +++ b/src/interfaces/position/IPositionManagerOwnerActions.sol @@ -9,7 +9,7 @@ interface IPositionManagerOwnerActions { /** * @notice Called by owners to burn an existing NFT. - * @dev Requires that all lp tokens have been removed from the NFT prior to calling. + * @dev Requires that all lps have been removed from the NFT prior to calling. * @param params Calldata struct supplying inputs required to update the underlying assets owed to an NFT. */ function burn( @@ -29,7 +29,8 @@ interface IPositionManagerOwnerActions { ) external; /** - * @notice Called by owners to add quote tokens and receive a representative NFT. + * @notice Called by owners to mint and receive an Ajna Position NFT. + * @dev PositionNFTs can only be minited with an association to pools that have been deployed by the Ajna ERC20PoolFactory or ERC721PoolFactory. * @param params Calldata struct supplying inputs required to mint a positions NFT. * @return tokenId The tokenId of the newly minted NFT. */ @@ -94,6 +95,7 @@ interface IPositionManagerOwnerActions { address pool; // The pool address associated with positions NFT uint256 fromIndex; // The bucket index from which liquidity should be moved uint256 toIndex; // The bucket index to which liquidity should be moved + uint256 expiry; // Timestamp after which this TX will revert, preventing inclusion in a block with unfavorable price } /** @@ -104,4 +106,4 @@ interface IPositionManagerOwnerActions { address pool; // The pool address associated with positions NFT uint256[] indexes; // The array of bucket indexes to reedem positions for } -} \ No newline at end of file +} diff --git a/src/interfaces/rewards/IRewardsManagerErrors.sol b/src/interfaces/rewards/IRewardsManagerErrors.sol index f02a09017..f9f1bb33d 100644 --- a/src/interfaces/rewards/IRewardsManagerErrors.sol +++ b/src/interfaces/rewards/IRewardsManagerErrors.sol @@ -11,6 +11,11 @@ interface IRewardsManagerErrors { */ error AlreadyClaimed(); + /** + * @notice User attempted to claim rewards for an epoch that is not yet available. + */ + error EpochNotAvailable(); + /** * @notice User attempted to record updated exchange rates outside of the allowed period. */ diff --git a/src/interfaces/rewards/IRewardsManagerState.sol b/src/interfaces/rewards/IRewardsManagerState.sol index 01ebd8a3b..0367f3cab 100644 --- a/src/interfaces/rewards/IRewardsManagerState.sol +++ b/src/interfaces/rewards/IRewardsManagerState.sol @@ -47,6 +47,18 @@ interface IRewardsManagerState { uint256 tokenId ) external view returns (address, address, uint256); + /** + * @notice Retrieve information about recorded LPs and rate values for a given bucket and a given stake, at stake time. + * @param tokenId ID of the NFT staked in the rewards contract to retrieve information about. + * @param bucketId ID of the bucket to retrieve recorded information at stake time. + * @return [WAD] LP amount the NFT owner is entitled in current bucket at the time of staking. + * @return [WAD] current bucket exchange rate at the time of staking. + */ + function getBucketStateStakeInfo( + uint256 tokenId, + uint256 bucketId + ) external view returns (uint256, uint256); + } /*********************/ @@ -62,6 +74,6 @@ struct StakeInfo { } struct BucketState { - uint256 lpsAtStakeTime; // [RAY] LP amount the NFT owner is entitled in current bucket at the time of staking - uint256 rateAtStakeTime; // [RAY] current bucket exchange rate at the time of staking (RAY) + uint256 lpsAtStakeTime; // [WAD] LP amount the NFT owner is entitled in current bucket at the time of staking + uint256 rateAtStakeTime; // [WAD] current bucket exchange rate at the time of staking } diff --git a/src/libraries/external/Auctions.sol b/src/libraries/external/Auctions.sol index cd7cc022b..16153864a 100644 --- a/src/libraries/external/Auctions.sol +++ b/src/libraries/external/Auctions.sol @@ -59,17 +59,13 @@ library Auctions { struct BucketTakeParams { address borrower; // borrower address to take from - uint256 collateral; // [WAD] borrower available collateral to take bool depositTake; // deposit or arb take, used by bucket take uint256 index; // bucket index, used by bucket take uint256 inflator; // [WAD] current pool inflator - uint256 t0Debt; // [WAD] borrower t0 debt uint256 collateralScale; // precision of collateral token based on decimals } struct TakeParams { address borrower; // borrower address to take from - uint256 collateral; // [WAD] borrower available collateral to take - uint256 t0Debt; // [WAD] borrower t0 debt uint256 takeCollateral; // [WAD] desired amount to take uint256 inflator; // [WAD] current pool inflator uint256 poolType; // pool type (ERC20 or NFT) @@ -84,13 +80,13 @@ library Auctions { uint256 amountToDebitFromDeposit; // [WAD] the amount of quote tokens used to kick and debited from lender deposit uint256 bucketCollateral; // [WAD] amount of collateral in bucket uint256 bucketDeposit; // [WAD] amount of quote tokens in bucket - uint256 bucketLPs; // [RAY] LPs of the bucket + uint256 bucketLPs; // [WAD] LPs of the bucket uint256 bucketPrice; // [WAD] bucket price - uint256 bucketRate; // [RAY] bucket exchange rate + uint256 bucketRate; // [WAD] bucket exchange rate uint256 bucketScale; // [WAD] bucket scales uint256 bucketUnscaledDeposit; // [WAD] unscaled amount of quote tokens in bucket - uint256 lenderLPs; // [RAY] LPs of lender in bucket - uint256 redeemedLPs; // [RAY] LPs used by kick action + uint256 lenderLPs; // [WAD] LPs of lender in bucket + uint256 redeemedLPs; // [WAD] LPs used by kick action } struct SettleLocalVars { uint256 collateralUsed; // [WAD] collateral used to settle debt @@ -122,20 +118,6 @@ library Auctions { uint256 unscaledDeposit; // [WAD] Unscaled bucket quantity uint256 unscaledQuoteTokenAmount; // [WAD] The unscaled token amount that taker should pay for collateral taken. } - struct TakeLoanLocalVars { - uint256 repaidDebt; // [WAD] the amount of debt repaid to th epool by take auction - uint256 borrowerDebt; // [WAD] the amount of borrower debt - bool inAuction; // true if loan in auction - } - struct TakeFromLoanLocalVars { - uint256 borrowerDebt; // [WAD] borrower's accrued debt - bool inAuction; // true if loan still in auction after auction is taken, false otherwise - uint256 newLup; // [WAD] LUP after auction is taken - uint256 repaidDebt; // [WAD] debt repaid when auction is taken - uint256 t0DebtInAuction; // [WAD] t0 pool debt in auction - uint256 t0DebtInAuctionChange; // [WAD] t0 change amount of debt after auction is taken - uint256 t0PoolDebt; // [WAD] t0 pool debt - } /**************/ /*** Events ***/ @@ -162,6 +144,7 @@ library Auctions { error AuctionNotClearable(); error AuctionPriceGtBucketPrice(); error BorrowerOk(); + error CollateralRoundingNeededButNotPossible(); error InsufficientLiquidity(); error InsufficientCollateral(); error NoAuction(); @@ -192,7 +175,6 @@ library Auctions { * - BucketBankruptcy * @param params_ Settle params * @return collateralRemaining_ The amount of borrower collateral left after settle. - * @return t0DebtRemaining_ The amount of t0 debt left after settle. * @return collateralSettled_ The amount of collateral settled. * @return t0DebtSettled_ The amount of t0 debt settled. */ @@ -204,7 +186,6 @@ library Auctions { SettleParams memory params_ ) external returns ( uint256 collateralRemaining_, - uint256 t0DebtRemaining_, uint256 collateralSettled_, uint256 t0DebtSettled_ ) { @@ -312,14 +293,13 @@ library Auctions { } } - t0DebtRemaining_ = borrower.t0Debt; - t0DebtSettled_ -= t0DebtRemaining_; + t0DebtSettled_ -= borrower.t0Debt; emit Settle(params_.borrower, t0DebtSettled_); if (borrower.t0Debt == 0) { // settle auction - borrower.collateral = _settleAuction( + (borrower.collateral, ) = _settleAuction( auctions_, buckets_, deposits_, @@ -379,7 +359,7 @@ library Auctions { DepositsState storage deposits_, mapping(uint256 => Bucket) storage buckets_, LoansState storage loans_, - PoolState memory poolState_, + PoolState calldata poolState_, uint256 index_ ) external returns ( KickResult memory kickResult_ @@ -406,7 +386,7 @@ library Auctions { vars.bucketPrice ); - vars.amountToDebitFromDeposit = Maths.rayToWad(Maths.rmul(vars.lenderLPs, vars.bucketRate)); // calculate amount to remove based on lender LPs in bucket + vars.amountToDebitFromDeposit = Maths.wmul(vars.lenderLPs, vars.bucketRate); // calculate amount to remove based on lender LPs in bucket if (vars.amountToDebitFromDeposit > vars.bucketDeposit) vars.amountToDebitFromDeposit = vars.bucketDeposit; // cap the amount to remove at bucket deposit @@ -444,7 +424,7 @@ library Auctions { Deposits.unscaledRemove(deposits_, index_, vars.bucketUnscaledDeposit); } else { - vars.redeemedLPs = Maths.wrdivr(vars.amountToDebitFromDeposit, vars.bucketRate); + vars.redeemedLPs = Maths.wdiv(vars.amountToDebitFromDeposit, vars.bucketRate); Deposits.unscaledRemove( deposits_, @@ -484,19 +464,20 @@ library Auctions { if (borrower.collateral == 0) revert InsufficientCollateral(); // revert if borrower's collateral is 0 + uint256 t0RepayAmount; + uint256 t0BorrowerDebt; ( result_.collateralAmount, - result_.t0RepayAmount, - borrower.t0Debt, + t0RepayAmount, + t0BorrowerDebt, result_.t0DebtPenalty ) = _takeBucket( auctions_, buckets_, deposits_, + borrower, BucketTakeParams({ borrower: borrowerAddress_, - collateral: borrower.collateral, - t0Debt: borrower.t0Debt, inflator: poolState_.inflator, depositTake: depositTake_, index: index_, @@ -505,16 +486,20 @@ library Auctions { ); borrower.collateral -= result_.collateralAmount; + borrower.t0Debt = t0BorrowerDebt - t0RepayAmount; - if (result_.t0DebtPenalty != 0) { - poolState_.debt += Maths.wmul(result_.t0DebtPenalty, poolState_.inflator); - } + // update pool debt: apply penalty if case + poolState_.t0Debt += result_.t0DebtPenalty; + poolState_.t0Debt -= t0RepayAmount; + poolState_.debt = Maths.wmul(poolState_.t0Debt, poolState_.inflator); + result_.t0PoolDebt = poolState_.t0Debt; + result_.poolDebt = poolState_.debt; ( - result_.poolDebt, result_.newLup, - result_.t0DebtInAuctionChange, - result_.settledAuction + result_.settledAuction, + result_.remainingCollateral, + result_.compensatedCollateral ) = _takeLoan( auctions_, buckets_, @@ -522,9 +507,16 @@ library Auctions { loans_, poolState_, borrower, - borrowerAddress_, - result_.t0RepayAmount + borrowerAddress_ ); + + if (result_.settledAuction) { + // the overall debt in auction change is the total borrower debt exiting auction + result_.t0DebtInAuctionChange = t0BorrowerDebt; + } else { + // the overall debt in auction change is the amount of partially repaid debt + result_.t0DebtInAuctionChange = t0RepayAmount; + } } /** @@ -547,22 +539,28 @@ library Auctions { ) external returns (TakeResult memory result_) { Borrower memory borrower = loans_.borrowers[borrowerAddress_]; - // revert if borrower's collateral is 0 or if maxCollateral to be taken is 0 - if (borrower.collateral == 0 || collateral_ == 0) revert InsufficientCollateral(); + if ( + (collateral_ == 0) || // revert if amount to take is 0 + (poolState_.poolType == uint8(PoolType.ERC721) && borrower.collateral < 1e18) || // revert in case of NFT take when there isn't a full token to be taken + (poolState_.poolType == uint8(PoolType.ERC20) && borrower.collateral == 0) // revert in case of ERC20 take when no collateral to be taken + ) { + revert InsufficientCollateral(); + } + uint256 t0RepayAmount; + uint256 t0BorrowerDebt; ( result_.collateralAmount, result_.quoteTokenAmount, - result_.t0RepayAmount, - borrower.t0Debt, + t0RepayAmount, + t0BorrowerDebt, result_.t0DebtPenalty, result_.excessQuoteToken ) = _take( auctions_, + borrower, TakeParams({ borrower: borrowerAddress_, - collateral: borrower.collateral, - t0Debt: borrower.t0Debt, takeCollateral: collateral_, inflator: poolState_.inflator, poolType: poolState_.poolType, @@ -571,16 +569,20 @@ library Auctions { ); borrower.collateral -= result_.collateralAmount; + borrower.t0Debt = t0BorrowerDebt - t0RepayAmount; - if (result_.t0DebtPenalty != 0) { - poolState_.debt += Maths.wmul(result_.t0DebtPenalty, poolState_.inflator); - } + // update pool debt: apply penalty if case + poolState_.t0Debt += result_.t0DebtPenalty; + poolState_.t0Debt -= t0RepayAmount; + poolState_.debt = Maths.wmul(poolState_.t0Debt, poolState_.inflator); + result_.t0PoolDebt = poolState_.t0Debt; + result_.poolDebt = poolState_.debt; ( - result_.poolDebt, result_.newLup, - result_.t0DebtInAuctionChange, - result_.settledAuction + result_.settledAuction, + result_.remainingCollateral, + result_.compensatedCollateral ) = _takeLoan( auctions_, buckets_, @@ -588,9 +590,16 @@ library Auctions { loans_, poolState_, borrower, - borrowerAddress_, - result_.t0RepayAmount + borrowerAddress_ ); + + if (result_.settledAuction) { + // the overall debt in auction change is the total borrower debt exiting auction + result_.t0DebtInAuctionChange = t0BorrowerDebt; + } else { + // the overall debt in auction change is the amount of partially repaid debt + result_.t0DebtInAuctionChange = t0RepayAmount; + } } /** @@ -611,7 +620,7 @@ library Auctions { uint256 curUnclaimedAuctionReserve = reserveAuction_.unclaimed; uint256 claimable = _claimableReserves( - Maths.wmul(params_.poolDebt, params_.inflator), + Maths.wmul(params_.t0PoolDebt, params_.inflator), params_.poolSize, auctions_.totalBondEscrowed, curUnclaimedAuctionReserve, @@ -670,10 +679,11 @@ library Auctions { * @notice Performs auction settle based on pool type, emits settle event and removes auction from auctions queue. * @dev emit events: * - AuctionNFTSettle or AuctionSettle - * @param borrowerAddress_ Address of the borrower that exits auction. - * @param borrowerCollateral_ Borrower collateral amount before auction exit (in NFT could be fragmented as result of partial takes). - * @param poolType_ Type of the pool (can be ERC20 or NFT). - * @return remainingCollateral_ Collateral remaining after auction is settled (same amount for ERC20 pool, rounded collateral for NFT pool). + * @param borrowerAddress_ Address of the borrower that exits auction. + * @param borrowerCollateral_ Borrower collateral amount before auction exit (in NFT could be fragmented as result of partial takes). + * @param poolType_ Type of the pool (can be ERC20 or NFT). + * @return remainingCollateral_ Collateral remaining after auction is settled (same amount for ERC20 pool, rounded collateral for NFT pool). + * @return compensatedCollateral_ Amount of collateral compensated (NFT settle only), to be deducted from pool pledged collateral accumulator. 0 for ERC20 pools. */ function _settleAuction( AuctionsState storage auctions_, @@ -682,18 +692,38 @@ library Auctions { address borrowerAddress_, uint256 borrowerCollateral_, uint256 poolType_ - ) internal returns (uint256 remainingCollateral_) { + ) internal returns (uint256 remainingCollateral_, uint256 compensatedCollateral_) { + if (poolType_ == uint8(PoolType.ERC721)) { uint256 lps; uint256 bucketIndex; - (remainingCollateral_, lps, bucketIndex) = _settleNFTCollateral( - auctions_, - buckets_, - deposits_, - borrowerAddress_, - borrowerCollateral_ - ); + remainingCollateral_ = (borrowerCollateral_ / Maths.WAD) * Maths.WAD; // floor collateral of borrower + + // if there's fraction of NFTs remaining then reward difference to borrower as LPs in auction price bucket + if (remainingCollateral_ != borrowerCollateral_) { + + // calculate the amount of collateral that should be compensated with LPs + compensatedCollateral_ = borrowerCollateral_ - remainingCollateral_; + + uint256 auctionPrice = _auctionPrice( + auctions_.liquidations[borrowerAddress_].kickMomp, + auctions_.liquidations[borrowerAddress_].neutralPrice, + auctions_.liquidations[borrowerAddress_].kickTime + ); + + // determine the bucket index to compensate fractional collateral + bucketIndex = auctionPrice > MIN_PRICE ? _indexOf(auctionPrice) : MAX_FENWICK_INDEX; + + // deposit collateral in bucket and reward LPs to compensate fractional collateral + lps = Buckets.addCollateral( + buckets_[bucketIndex], + borrowerAddress_, + Deposits.valueAt(deposits_, bucketIndex), + compensatedCollateral_, + _priceAt(bucketIndex) + ); + } emit AuctionNFTSettle(borrowerAddress_, remainingCollateral_, lps, bucketIndex); @@ -706,46 +736,6 @@ library Auctions { _removeAuction(auctions_, borrowerAddress_); } - /** - * @notice Performs NFT collateral settlement by rounding down borrower's collateral amount and by moving borrower's token ids to pool claimable array. - * @param borrowerAddress_ Address of the borrower that exits auction. - * @param borrowerCollateral_ Borrower collateral amount before auction exit (could be fragmented as result of partial takes). - * @return floorCollateral_ Rounded down collateral, the number of NFT tokens borrower can pull after auction exit. - * @return lps_ LPs given to the borrower to compensate fractional collateral (if any). - * @return bucketIndex_ Index of the bucket with LPs to compensate. - */ - function _settleNFTCollateral( - AuctionsState storage auctions_, - mapping(uint256 => Bucket) storage buckets_, - DepositsState storage deposits_, - address borrowerAddress_, - uint256 borrowerCollateral_ - ) internal returns (uint256 floorCollateral_, uint256 lps_, uint256 bucketIndex_) { - floorCollateral_ = (borrowerCollateral_ / Maths.WAD) * Maths.WAD; // floor collateral of borrower - - // if there's fraction of NFTs remaining then reward difference to borrower as LPs in auction price bucket - if (floorCollateral_ != borrowerCollateral_) { - // cover borrower's fractional amount with LPs in auction price bucket - uint256 fractionalCollateral = borrowerCollateral_ - floorCollateral_; - - uint256 auctionPrice = _auctionPrice( - auctions_.liquidations[borrowerAddress_].kickMomp, - auctions_.liquidations[borrowerAddress_].neutralPrice, - auctions_.liquidations[borrowerAddress_].kickTime - ); - - bucketIndex_ = auctionPrice > MIN_PRICE ? _indexOf(auctionPrice) : MAX_FENWICK_INDEX; - - lps_ = Buckets.addCollateral( - buckets_[bucketIndex_], - borrowerAddress_, - Deposits.valueAt(deposits_, bucketIndex_), - fractionalCollateral, - _priceAt(bucketIndex_) - ); - } - } - /** * @notice Called to start borrower liquidation and to update the auctions queue. * @dev write state: @@ -770,7 +760,7 @@ library Auctions { AuctionsState storage auctions_, DepositsState storage deposits_, LoansState storage loans_, - PoolState memory poolState_, + PoolState calldata poolState_, address borrowerAddress_, uint256 additionalDebt_ ) internal returns ( @@ -806,10 +796,6 @@ library Auctions { momp ); - // when loan is kicked, penalty of three months of interest is added - kickResult_.kickPenalty = Maths.wmul(Maths.wdiv(poolState_.rate, 4 * 1e18), borrowerDebt); - kickResult_.t0KickPenalty = Maths.wdiv(kickResult_.kickPenalty, poolState_.inflator); - // record liquidation info uint256 neutralPrice = Maths.wmul(borrower.t0Np, poolState_.inflator); _recordAuction( @@ -827,14 +813,20 @@ library Auctions { // remove kicked loan from heap Loans.remove(loans_, borrowerAddress_, loans_.indices[borrowerAddress_]); - kickResult_.t0KickedDebt += kickResult_.t0KickPenalty; + // when loan is kicked, penalty of three months of interest is added + uint256 t0KickPenalty = Maths.wmul(kickResult_.t0KickedDebt, Maths.wdiv(poolState_.rate, 4 * 1e18)); + uint256 kickPenalty = Maths.wmul(t0KickPenalty, poolState_.inflator); + + kickResult_.t0PoolDebt = poolState_.t0Debt + t0KickPenalty; + kickResult_.t0KickedDebt += t0KickPenalty; + // update borrower debt with kicked debt penalty borrower.t0Debt = kickResult_.t0KickedDebt; emit Kick( borrowerAddress_, - borrowerDebt + kickResult_.kickPenalty, - borrower.collateral, + borrowerDebt + kickPenalty, + borrowerCollateral, bondSize ); } @@ -843,7 +835,8 @@ library Auctions { * @notice Performs take collateral on an auction and updates bond size and kicker balance accordingly. * @dev emit events: * - Take - * @param params_ Struct containing take action params details. + * @param borrower_ Struct containing auctioned borrower details. + * @param params_ Struct containing take action params details. * @return Collateral amount taken. * @return Quote token to be received from taker. * @return T0 debt amount repaid. @@ -853,22 +846,34 @@ library Auctions { */ function _take( AuctionsState storage auctions_, + Borrower memory borrower_, TakeParams memory params_ ) internal returns (uint256, uint256, uint256, uint256, uint256, uint256) { Liquidation storage liquidation = auctions_.liquidations[params_.borrower]; - TakeLocalVars memory vars = _prepareTake(liquidation, params_.t0Debt, params_.collateral, params_.inflator); + TakeLocalVars memory vars = _prepareTake( + liquidation, + borrower_.t0Debt, + borrower_.collateral, + params_.inflator + ); // These are placeholder max values passed to calculateTakeFlows because there is no explicit bound on the // quote token amount in take calls (as opposed to bucketTake) vars.unscaledDeposit = type(uint256).max; vars.bucketScale = Maths.WAD; + uint256 takeableCollateral = borrower_.collateral; + // for NFT take make sure the take flow and bond change calculation happens for the rounded collateral that can be taken + if (params_.poolType == uint8(PoolType.ERC721)) { + takeableCollateral = (takeableCollateral / 1e18) * 1e18; + } + // In the case of take, the taker binds the collateral qty but not the quote token qty // ugly to get take work like a bucket take -- this is the max amount of quote token from the take that could go to // reduce the debt of the borrower -- analagous to the amount of deposit in the bucket for a bucket take vars = _calculateTakeFlowsAndBondChange( - Maths.min(params_.collateral, params_.takeCollateral), + Maths.min(takeableCollateral, params_.takeCollateral), params_.inflator, params_.collateralScale, vars @@ -888,14 +893,18 @@ library Auctions { // slither-disable-next-line divide-before-multiply uint256 collateralTaken = (vars.collateralAmount / 1e18) * 1e18; // solidity rounds down, so if 2.5 it will be 2.5 / 1 = 2 - if (collateralTaken != vars.collateralAmount && params_.collateral >= collateralTaken + 1e18) { // collateral taken not a round number - collateralTaken += 1e18; // round up collateral to take - // taker should send additional quote tokens to cover difference between collateral needed to be taken and rounded collateral, at auction price - // borrower will get quote tokens for the difference between rounded collateral and collateral taken to cover debt - vars.excessQuoteToken = Maths.wmul(collateralTaken - vars.collateralAmount, vars.auctionPrice); + if (collateralTaken != vars.collateralAmount) { // collateral taken not a round number + if (Maths.min(borrower_.collateral, params_.takeCollateral) >= collateralTaken + 1e18) { + collateralTaken += 1e18; // round up collateral to take + // taker should send additional quote tokens to cover difference between collateral needed to be taken and rounded collateral, at auction price + // borrower will get quote tokens for the difference between rounded collateral and collateral taken to cover debt + vars.excessQuoteToken = Maths.wmul(collateralTaken - vars.collateralAmount, vars.auctionPrice); + vars.collateralAmount = collateralTaken; + } else { + // shouldn't get here, but just in case revert + revert CollateralRoundingNeededButNotPossible(); + } } - - vars.collateralAmount = collateralTaken; } return ( @@ -912,7 +921,8 @@ library Auctions { * @notice Performs bucket take collateral on an auction and rewards taker and kicker (if case). * @dev emit events: * - BucketTake - * @param params_ Struct containing take action details. + * @param borrower_ Struct containing auctioned borrower details. + * @param params_ Struct containing take action details. * @return Collateral amount taken. * @return T0 debt amount repaid. * @return T0 borrower debt (including penalty). @@ -922,12 +932,18 @@ library Auctions { AuctionsState storage auctions_, mapping(uint256 => Bucket) storage buckets_, DepositsState storage deposits_, + Borrower memory borrower_, BucketTakeParams memory params_ ) internal returns (uint256, uint256, uint256, uint256) { Liquidation storage liquidation = auctions_.liquidations[params_.borrower]; - TakeLocalVars memory vars = _prepareTake(liquidation, params_.t0Debt, params_.collateral, params_.inflator); + TakeLocalVars memory vars = _prepareTake( + liquidation, + borrower_.t0Debt, + borrower_.collateral, + params_.inflator + ); vars.unscaledDeposit = Deposits.unscaledValueAt(deposits_, params_.index); @@ -944,7 +960,7 @@ library Auctions { vars.bucketScale = Deposits.scale(deposits_, params_.index); vars = _calculateTakeFlowsAndBondChange( - params_.collateral, + borrower_.collateral, params_.inflator, params_.collateralScale, vars @@ -982,13 +998,13 @@ library Auctions { * @notice If borrower becomes recollateralized then auction is settled. Update loan's state. * @dev reverts on: * - borrower debt less than pool min debt AmountLTMinDebt() - * @param borrower_ The borrower details owning loan that is taken. - * @param borrowerAddress_ The address of the borrower. - * @param t0RepaidDebt_ T0 debt amount repaid by the take action. - * @return poolDebt_ Accrued debt pool after debt is repaid. - * @return newLup_ The new LUP of pool (after debt is repaid). - * @return t0DebtInAuctionChange_ The overall debt in auction change (remaining borrower debt if auction settled, repaid debt otherwise). - * @return settledAuction_ True if auction is settled by the take action. + * @param borrower_ Struct containing pool details. + * @param borrower_ The borrower details owning loan that is taken. + * @param borrowerAddress_ The address of the borrower. + * @return newLup_ The new LUP of pool (after debt is repaid). + * @return settledAuction_ True if auction is settled by the take action. (NFT take: rebalance borrower collateral in pool if true) + * @return remainingCollateral_ Borrower collateral remaining after take action. (NFT take: collateral to be rebalanced in case of NFT settlement) + * @return compensatedCollateral_ Amount of collateral compensated, to be deducted from pool pledged collateral accumulator. */ function _takeLoan( AuctionsState storage auctions_, @@ -997,41 +1013,34 @@ library Auctions { LoansState storage loans_, PoolState memory poolState_, Borrower memory borrower_, - address borrowerAddress_, - uint256 t0RepaidDebt_ + address borrowerAddress_ ) internal returns ( - uint256 poolDebt_, uint256 newLup_, - uint256 t0DebtInAuctionChange_, - bool settledAuction_ + bool settledAuction_, + uint256 remainingCollateral_, + uint256 compensatedCollateral_ ) { - TakeLoanLocalVars memory vars; - - vars.repaidDebt = Maths.wmul(t0RepaidDebt_, poolState_.inflator); - vars.borrowerDebt = Maths.wmul(borrower_.t0Debt, poolState_.inflator); - - vars.borrowerDebt -= vars.repaidDebt; - poolDebt_ = poolState_.debt - vars.repaidDebt; + uint256 borrowerDebt = Maths.wmul(borrower_.t0Debt, poolState_.inflator); // check that taking from loan doesn't leave borrower debt under min debt amount - _revertOnMinDebt(loans_, poolDebt_, vars.borrowerDebt, poolState_.quoteDustLimit); - - newLup_ = _lup(deposits_, poolDebt_); + _revertOnMinDebt( + loans_, + poolState_.debt, + borrowerDebt, + poolState_.quoteDustLimit + ); - vars.inAuction = true; + // calculate new lup with repaid debt from take + newLup_ = _lup(deposits_, poolState_.debt); - if (_isCollateralized(vars.borrowerDebt, borrower_.collateral, newLup_, poolState_.poolType)) { - // settle auction if borrower becomes re-collateralized + remainingCollateral_ = borrower_.collateral; - vars.inAuction = false; + if (_isCollateralized(borrowerDebt, borrower_.collateral, newLup_, poolState_.poolType)) { settledAuction_ = true; - // the overall debt in auction change is the total borrower debt exiting auction - t0DebtInAuctionChange_ = borrower_.t0Debt; - // settle auction and update borrower's collateral with value after settlement - borrower_.collateral = _settleAuction( + (remainingCollateral_, compensatedCollateral_) = _settleAuction( auctions_, buckets_, deposits_, @@ -1039,12 +1048,9 @@ library Auctions { borrower_.collateral, poolState_.poolType ); - } else { - // the overall debt in auction change is the amount of partially repaid debt - t0DebtInAuctionChange_ = t0RepaidDebt_; - } - borrower_.t0Debt -= t0RepaidDebt_; + borrower_.collateral = remainingCollateral_; + } // update loan state, stamp borrower t0Np only when exiting from auction Loans.update( @@ -1053,11 +1059,11 @@ library Auctions { deposits_, borrower_, borrowerAddress_, - vars.borrowerDebt, + poolState_.debt, poolState_.rate, newLup_, - vars.inAuction, - !vars.inAuction // stamp borrower t0Np if exiting from auction + !settledAuction_, + settledAuction_ // stamp borrower t0Np if exiting from auction ); } @@ -1146,6 +1152,8 @@ library Auctions { vars.t0RepayAmount = Maths.wdiv(vars.scaledQuoteTokenAmount, inflator_); vars.unscaledQuoteTokenAmount = vars.unscaledDeposit; + vars.scaledQuoteTokenAmount = Maths.wmul(vars.collateralAmount, vars.auctionPrice); + } else if (vars.borrowerDebt <= borrowerCollateralValue) { // borrower debt is constraining factor vars.collateralAmount = _roundToScale(Maths.wdiv(vars.borrowerDebt, borrowerPrice), collateralScale_); @@ -1164,7 +1172,7 @@ library Auctions { } if (vars.isRewarded) { - // take is above neutralPrice, Kicker is rewarded + // take is below neutralPrice, Kicker is rewarded vars.bondChange = Maths.wmul(vars.scaledQuoteTokenAmount, uint256(vars.bpf)); } else { // take is above neutralPrice, Kicker is penalized @@ -1332,11 +1340,12 @@ library Auctions { ) internal { Bucket storage bucket = buckets_[bucketIndex_]; - uint256 bucketExchangeRate = Buckets.getUnscaledExchangeRate( + uint256 scaledDeposit = Maths.wmul(vars.unscaledDeposit, vars.bucketScale); + + uint256 exchangeRate = Buckets.getExchangeRate( bucket.collateral, bucket.lps, - vars.unscaledDeposit, - vars.bucketScale, + scaledDeposit, vars.bucketPrice ); @@ -1345,10 +1354,9 @@ library Auctions { // if arb take - taker is awarded collateral * (bucket price - auction price) worth (in quote token terms) units of LPB in the bucket if (!depositTake_) { - uint256 takerReward = Maths.wmul(vars.collateralAmount, vars.bucketPrice - vars.auctionPrice); - uint256 takerRewardUnscaledQuoteToken = Maths.wdiv(takerReward, vars.bucketScale); + uint256 takerReward = Maths.wmul(vars.collateralAmount, vars.bucketPrice - vars.auctionPrice); - totalLPsReward = Maths.wrdivr(takerRewardUnscaledQuoteToken, bucketExchangeRate); + totalLPsReward = Maths.wdiv(takerReward, exchangeRate); Buckets.addLenderLPs(bucket, bankruptcyTime, msg.sender, totalLPsReward); } @@ -1357,7 +1365,7 @@ library Auctions { // the bondholder/kicker is awarded bond change worth of LPB in the bucket if (vars.isRewarded) { - kickerLPsReward = Maths.wrdivr(Maths.wdiv(vars.bondChange, vars.bucketScale), bucketExchangeRate); + kickerLPsReward = Maths.wdiv(vars.bondChange, exchangeRate); totalLPsReward += kickerLPsReward; Buckets.addLenderLPs(bucket, bankruptcyTime, vars.kicker, kickerLPsReward); diff --git a/src/libraries/external/BorrowerActions.sol b/src/libraries/external/BorrowerActions.sol index 5fb1f70a5..04e5b020e 100644 --- a/src/libraries/external/BorrowerActions.sol +++ b/src/libraries/external/BorrowerActions.sol @@ -20,7 +20,10 @@ import { _priceAt, _isCollateralized } from '../helpers/PoolHelper.sol'; -import { _revertOnMinDebt } from '../helpers/RevertsHelper.sol'; +import { + _revertIfLupDroppedBelowLimit, + _revertOnMinDebt +} from '../helpers/RevertsHelper.sol'; import { Buckets } from '../internal/Buckets.sol'; import { Deposits } from '../internal/Deposits.sol'; @@ -41,16 +44,19 @@ library BorrowerActions { /*************************/ struct DrawDebtLocalVars { - uint256 borrowerDebt; // [WAD] borrower's accrued debt - uint256 debtChange; // [WAD] additional debt resulted from draw debt action - bool inAuction; // true if loan is auctioned - uint256 lupId; // id of new LUP - bool stampT0Np; // true if loan's t0 neutral price should be restamped (when drawing debt or pledge settles auction) + bool borrow; // true if borrow action + uint256 borrowerDebt; // [WAD] borrower's accrued debt + uint256 compensatedCollateral; // [WAD] amount of borrower collateral that is compensated with LPs (NFTs only) + uint256 t0BorrowAmount; // [WAD] t0 amount to borrow + uint256 t0DebtChange; // [WAD] additional t0 debt resulted from draw debt action + bool inAuction; // true if loan is auctioned + bool pledge; // true if pledge action + bool stampT0Np; // true if loan's t0 neutral price should be restamped (when drawing debt or pledge settles auction) } struct RepayDebtLocalVars { uint256 borrowerDebt; // [WAD] borrower's accrued debt + uint256 compensatedCollateral; // [WAD] amount of borrower collateral that is compensated with LPs (NFTs only) bool inAuction; // true if loan still in auction after repay, false otherwise - uint256 newLup; // [WAD] LUP after repay debt action bool pull; // true if pull action bool repay; // true if repay action bool stampT0Np; // true if loan's t0 neutral price should be restamped (when repay settles auction or pull collateral) @@ -63,10 +69,11 @@ library BorrowerActions { /**************/ // See `IPoolErrors` for descriptions + error AuctionActive(); error BorrowerNotSender(); error BorrowerUnderCollateralized(); error InsufficientCollateral(); - error LimitIndexReached(); + error LimitIndexExceeded(); error NoDebt(); /***************************/ @@ -91,7 +98,7 @@ library BorrowerActions { * @dev reverts on: * - borrower not sender BorrowerNotSender() * - borrower debt less than pool min debt AmountLTMinDebt() - * - limit price reached LimitIndexReached() + * - limit price reached LimitIndexExceeded() * - borrower cannot draw more debt BorrowerUnderCollateralized() * @dev emit events: * - Auctions._settleAuction: @@ -112,21 +119,27 @@ library BorrowerActions { ) { Borrower memory borrower = loans_.borrowers[borrowerAddress_]; - result_.poolDebt = poolState_.debt; - result_.newLup = _lup(deposits_, result_.poolDebt); - result_.poolCollateral = poolState_.collateral; - DrawDebtLocalVars memory vars; + vars.pledge = collateralToPledge_ != 0; + vars.borrow = amountToBorrow_ != 0 || limitIndex_ != 0; // enable an intentional 0 borrow loan call to update borrower's loan state vars.borrowerDebt = Maths.wmul(borrower.t0Debt, poolState_.inflator); + vars.inAuction = _inAuction(auctions_, borrowerAddress_); + + result_.t0PoolDebt = poolState_.t0Debt; + result_.poolDebt = poolState_.debt; + result_.poolCollateral = poolState_.collateral; - // pledge collateral to pool - if (collateralToPledge_ != 0) { + result_.remainingCollateral = borrower.collateral; + + if (vars.pledge) { // add new amount of collateral to pledge to borrower balance borrower.collateral += collateralToPledge_; - // load loan's auction state - vars.inAuction = _inAuction(auctions_, borrowerAddress_); + result_.remainingCollateral += collateralToPledge_; + + result_.newLup = _lup(deposits_, result_.poolDebt); + // if loan is auctioned and becomes collateralized by newly pledged collateral then settle auction if ( vars.inAuction && @@ -142,7 +155,10 @@ library BorrowerActions { result_.t0DebtInAuctionChange = borrower.t0Debt; // settle auction and update borrower's collateral with value after settlement - result_.remainingCollateral = Auctions._settleAuction( + ( + result_.remainingCollateral, + vars.compensatedCollateral + ) = Auctions._settleAuction( auctions_, buckets_, deposits_, @@ -151,37 +167,49 @@ library BorrowerActions { poolState_.poolType ); - borrower.collateral = result_.remainingCollateral; + borrower.collateral = result_.remainingCollateral; + result_.poolCollateral -= vars.compensatedCollateral; } // add new amount of collateral to pledge to pool balance result_.poolCollateral += collateralToPledge_; } - // borrow against pledged collateral - // check both values to enable an intentional 0 borrow loan call to update borrower's loan state - if (amountToBorrow_ != 0 || limitIndex_ != 0) { + if (vars.borrow) { // only intended recipient can borrow quote if (borrowerAddress_ != msg.sender) revert BorrowerNotSender(); - // add origination fee to the amount to borrow and add to borrower's debt - vars.debtChange = Maths.wmul(amountToBorrow_, _feeRate(poolState_.rate) + Maths.WAD); + // an auctioned borrower in not allowed to draw more debt (even if collateralized at the new LUP) if auction is not settled + if (vars.inAuction) revert AuctionActive(); + + vars.t0BorrowAmount = Maths.wdiv(amountToBorrow_, poolState_.inflator); + + // t0 debt change is t0 amount to borrow plus the origination fee + vars.t0DebtChange = Maths.wmul(vars.t0BorrowAmount, _feeRate(poolState_.rate) + Maths.WAD); + + borrower.t0Debt += vars.t0DebtChange; - vars.borrowerDebt += vars.debtChange; + vars.borrowerDebt = Maths.wmul(borrower.t0Debt, poolState_.inflator); - // check that drawing debt doesn't leave borrower debt under min debt amount - _revertOnMinDebt(loans_, result_.poolDebt, vars.borrowerDebt, poolState_.quoteDustLimit); + // check that drawing debt doesn't leave borrower debt under pool min debt amount + _revertOnMinDebt( + loans_, + result_.poolDebt, + vars.borrowerDebt, + poolState_.quoteDustLimit + ); // add debt change to pool's debt - result_.poolDebt += vars.debtChange; + result_.t0PoolDebt += vars.t0DebtChange; + result_.poolDebt = Maths.wmul(result_.t0PoolDebt, poolState_.inflator); - // determine new lup index and revert if borrow happens at a price higher than the specified limit (lower index than lup index) - vars.lupId = _lupIndex(deposits_, result_.poolDebt); - if (vars.lupId > limitIndex_) revert LimitIndexReached(); + result_.newLup = _lup(deposits_, result_.poolDebt); + + // revert if borrow drives LUP price under the specified price limit + _revertIfLupDroppedBelowLimit(result_.newLup, limitIndex_); // calculate new lup and check borrow action won't push borrower into a state of under-collateralization // this check also covers the scenario when loan is already auctioned - result_.newLup = _priceAt(vars.lupId); if (!_isCollateralized(vars.borrowerDebt, borrower.collateral, result_.newLup, poolState_.poolType)) { revert BorrowerUnderCollateralized(); @@ -189,10 +217,11 @@ library BorrowerActions { // stamp borrower t0Np when draw debt vars.stampT0Np = true; + } - result_.t0DebtChange = Maths.wdiv(vars.debtChange, poolState_.inflator); - - borrower.t0Debt += result_.t0DebtChange; + // calculate LUP if it wasn't calculated previously + if (!vars.pledge && !vars.borrow) { + result_.newLup = _lup(deposits_, result_.poolDebt); } // update loan state @@ -202,7 +231,7 @@ library BorrowerActions { deposits_, borrower, borrowerAddress_, - vars.borrowerDebt, + result_.poolDebt, poolState_.rate, result_.newLup, vars.inAuction, @@ -230,6 +259,7 @@ library BorrowerActions { * - borrower debt less than pool min debt AmountLTMinDebt() * - borrower not sender BorrowerNotSender() * - not enough collateral to pull InsufficientCollateral() + * - limit price reached LimitIndexExceeded() * @dev emit events: * - Auctions._settleAuction: * - AuctionNFTSettle or AuctionSettle @@ -242,7 +272,8 @@ library BorrowerActions { PoolState calldata poolState_, address borrowerAddress_, uint256 maxQuoteTokenAmountToRepay_, - uint256 collateralAmountToPull_ + uint256 collateralAmountToPull_, + uint256 limitIndex_ ) external returns ( RepayDebtResult memory result_ ) { @@ -251,35 +282,45 @@ library BorrowerActions { RepayDebtLocalVars memory vars; vars.repay = maxQuoteTokenAmountToRepay_ != 0; - vars.pull = collateralAmountToPull_ != 0; + vars.pull = collateralAmountToPull_ != 0; vars.borrowerDebt = Maths.wmul(borrower.t0Debt, poolState_.inflator); + vars.inAuction = _inAuction(auctions_, borrowerAddress_); + result_.t0PoolDebt = poolState_.t0Debt; result_.poolDebt = poolState_.debt; result_.poolCollateral = poolState_.collateral; + result_.remainingCollateral = borrower.collateral; + if (vars.repay) { if (borrower.t0Debt == 0) revert NoDebt(); if (maxQuoteTokenAmountToRepay_ == type(uint256).max) { - result_.t0RepaidDebt = borrower.t0Debt; + vars.t0RepaidDebt = borrower.t0Debt; } else { - result_.t0RepaidDebt = Maths.min( + vars.t0RepaidDebt = Maths.min( borrower.t0Debt, Maths.wdiv(maxQuoteTokenAmountToRepay_, poolState_.inflator) ); } - result_.quoteTokenToRepay = Maths.wmul(result_.t0RepaidDebt, poolState_.inflator); + result_.t0PoolDebt -= vars.t0RepaidDebt; - result_.poolDebt -= result_.quoteTokenToRepay; - vars.borrowerDebt -= result_.quoteTokenToRepay; + result_.poolDebt = Maths.wmul(result_.t0PoolDebt, poolState_.inflator); + result_.quoteTokenToRepay = Maths.wmul(vars.t0RepaidDebt, poolState_.inflator); + vars.borrowerDebt = Maths.wmul(borrower.t0Debt - vars.t0RepaidDebt, poolState_.inflator); // check that paying the loan doesn't leave borrower debt under min debt amount - _revertOnMinDebt(loans_, result_.poolDebt, vars.borrowerDebt, poolState_.quoteDustLimit); + _revertOnMinDebt( + loans_, + result_.poolDebt, + vars.borrowerDebt, + poolState_.quoteDustLimit + ); result_.newLup = _lup(deposits_, result_.poolDebt); - vars.inAuction = _inAuction(auctions_, borrowerAddress_); + // if loan is auctioned and becomes collateralized by repaying debt then settle auction if (vars.inAuction) { if (_isCollateralized(vars.borrowerDebt, borrower.collateral, result_.newLup, poolState_.poolType)) { // borrower becomes re-collateralized @@ -292,7 +333,10 @@ library BorrowerActions { result_.t0DebtInAuctionChange = borrower.t0Debt; // settle auction and update borrower's collateral with value after settlement - result_.remainingCollateral = Auctions._settleAuction( + ( + result_.remainingCollateral, + vars.compensatedCollateral + ) = Auctions._settleAuction( auctions_, buckets_, deposits_, @@ -301,23 +345,29 @@ library BorrowerActions { poolState_.poolType ); - borrower.collateral = result_.remainingCollateral; + borrower.collateral = result_.remainingCollateral; + result_.poolCollateral -= vars.compensatedCollateral; } else { // partial repay, remove only the paid debt from pool auctions debt accumulator - result_.t0DebtInAuctionChange = result_.t0RepaidDebt; + result_.t0DebtInAuctionChange = vars.t0RepaidDebt; } } - borrower.t0Debt -= result_.t0RepaidDebt; + borrower.t0Debt -= vars.t0RepaidDebt; } if (vars.pull) { // only intended recipient can pull collateral if (borrowerAddress_ != msg.sender) revert BorrowerNotSender(); - // calculate LUP only if it wasn't calculated by repay action + // an auctioned borrower in not allowed to pull collateral (even if collateralized at the new LUP) if auction is not settled + if (vars.inAuction) revert AuctionActive(); + + // calculate LUP only if it wasn't calculated in repay action if (!vars.repay) result_.newLup = _lup(deposits_, result_.poolDebt); + _revertIfLupDroppedBelowLimit(result_.newLup, limitIndex_); + uint256 encumberedCollateral = borrower.t0Debt != 0 ? Maths.wdiv(vars.borrowerDebt, result_.newLup) : 0; if (borrower.collateral - encumberedCollateral < collateralAmountToPull_) revert InsufficientCollateral(); @@ -329,7 +379,7 @@ library BorrowerActions { result_.poolCollateral -= collateralAmountToPull_; } - // calculate LUP if repay is called with 0 amount + // calculate LUP if it wasn't calculated previously if (!vars.repay && !vars.pull) { result_.newLup = _lup(deposits_, result_.poolDebt); } @@ -341,7 +391,7 @@ library BorrowerActions { deposits_, borrower, borrowerAddress_, - vars.borrowerDebt, + result_.poolDebt, poolState_.rate, result_.newLup, vars.inAuction, @@ -366,18 +416,11 @@ library BorrowerActions { return auctions_.liquidations[borrower_].kickTime != 0; } - function _lupIndex( - DepositsState storage deposits_, - uint256 debt_ - ) internal view returns (uint256) { - return Deposits.findIndexOfSum(deposits_, debt_); - } - function _lup( DepositsState storage deposits_, uint256 debt_ ) internal view returns (uint256) { - return _priceAt(_lupIndex(deposits_, debt_)); + return _priceAt(Deposits.findIndexOfSum(deposits_, debt_)); } } diff --git a/src/libraries/external/LenderActions.sol b/src/libraries/external/LenderActions.sol index f38bf6ea0..91fbf4d6f 100644 --- a/src/libraries/external/LenderActions.sol +++ b/src/libraries/external/LenderActions.sol @@ -34,19 +34,26 @@ library LenderActions { /*************************/ struct MoveQuoteLocalVars { - uint256 amountToMove; // [WAD] Quote token amount to move between indexes. - uint256 fromBucketPrice; // [WAD] Price of the bucket to move amount from. - uint256 fromBucketLPs; // [RAY] Amount of LPs in the bucket to move amount from. - uint256 fromBucketDepositTime; // Time of lender deposit in the bucket to move amount from. - uint256 toBucketPrice; // [WAD] Price of the bucket to move amount to. - uint256 toBucketBankruptcyTime; // Time the bucket to move amount to was marked as insolvent. - uint256 ptp; // [WAD] Pool Threshold Price. - uint256 htp; // [WAD] Highest Threshold Price. + uint256 amountToMove; // [WAD] Quote token amount to move between indexes. + uint256 fromBucketPrice; // [WAD] Price of the bucket to move amount from. + uint256 fromBucketCollateral; // [WAD] Total amount of collateral in from bucket. + uint256 fromBucketLPs; // [WAD] Total amount of LPs in from bucket. + uint256 fromBucketLenderLPs; // [WAD] Amount of LPs owned by lender in from bucket. + uint256 fromBucketDepositTime; // Time of lender deposit in the bucket to move amount from. + uint256 fromBucketRemainingLPs; // Amount of LPs remaining in from bucket after move. + uint256 fromBucketRemainingDeposit; // Amount of scaled deposit remaining in from bucket after move. + uint256 toBucketPrice; // [WAD] Price of the bucket to move amount to. + uint256 toBucketBankruptcyTime; // Time the bucket to move amount to was marked as insolvent. + uint256 toBucketUnscaledDeposit; // Amount of unscaled deposit in to bucket. + uint256 toBucketDeposit; // Amount of scaled deposit in to bucket. + uint256 toBucketScale; // Scale deposit of to bucket. + uint256 ptp; // [WAD] Pool Threshold Price. + uint256 htp; // [WAD] Highest Threshold Price. } struct RemoveDepositParams { uint256 depositConstraint; // [WAD] Constraint on deposit in quote token. - uint256 lpConstraint; // [RAY] Constraint in LPB terms. - uint256 bucketLPs; // [RAY] Total LPB in the bucket. + uint256 lpConstraint; // [WAD] Constraint in LPB terms. + uint256 bucketLPs; // [WAD] Total LPB in the bucket. uint256 bucketCollateral; // [WAD] Claimable collateral in the bucket. uint256 price; // [WAD] Price of bucket. uint256 index; // Bucket index. @@ -62,7 +69,7 @@ library LenderActions { event BucketBankruptcy(uint256 indexed index, uint256 lpForfeited); event MoveQuoteToken(address indexed lender, uint256 indexed from, uint256 indexed to, uint256 amount, uint256 lpRedeemedFrom, uint256 lpAwardedTo, uint256 lup); event RemoveQuoteToken(address indexed lender, uint256 indexed price, uint256 amount, uint256 lpRedeemed, uint256 lup); - event TransferLPTokens(address owner, address newOwner, uint256[] indexes, uint256 lpTokens); + event TransferLPs(address owner, address newOwner, uint256[] indexes, uint256 lps); /**************/ /*** Errors ***/ @@ -80,6 +87,7 @@ library LenderActions { error InsufficientLiquidity(); error InsufficientCollateral(); error MoveToSamePrice(); + error TransferToSameOwner(); /***************************/ /*** External Functions ***/ @@ -191,6 +199,7 @@ library LenderActions { * - dust amount DustAmountNotExceeded() * - invalid index InvalidIndex() * @dev emit events: + * - BucketBankruptcy * - MoveQuoteToken */ function moveQuoteToken( @@ -218,18 +227,21 @@ library LenderActions { Lender storage fromBucketLender = fromBucket.lenders[msg.sender]; vars.fromBucketPrice = _priceAt(params_.fromIndex); - vars.toBucketPrice = _priceAt(params_.toIndex); + vars.fromBucketCollateral = fromBucket.collateral; + vars.fromBucketLPs = fromBucket.lps; vars.fromBucketDepositTime = fromBucketLender.depositTime; - if (fromBucket.bankruptcyTime < vars.fromBucketDepositTime) vars.fromBucketLPs = fromBucketLender.lps; + vars.toBucketPrice = _priceAt(params_.toIndex); + + if (fromBucket.bankruptcyTime < vars.fromBucketDepositTime) vars.fromBucketLenderLPs = fromBucketLender.lps; - (vars.amountToMove, fromBucketRedeemedLPs_, ) = _removeMaxDeposit( + (vars.amountToMove, fromBucketRedeemedLPs_, vars.fromBucketRemainingDeposit) = _removeMaxDeposit( deposits_, RemoveDepositParams({ depositConstraint: params_.maxAmountToMove, - lpConstraint: vars.fromBucketLPs, - bucketLPs: fromBucket.lps, - bucketCollateral: fromBucket.collateral, + lpConstraint: vars.fromBucketLenderLPs, + bucketLPs: vars.fromBucketLPs, + bucketCollateral: vars.fromBucketCollateral, price: vars.fromBucketPrice, index: params_.fromIndex, dustLimit: poolState_.quoteDustLimit @@ -245,41 +257,53 @@ library LenderActions { } } - uint256 unscaledToBucketDeposit = Deposits.unscaledValueAt(deposits_, params_.toIndex); - uint256 toBucketScale = Deposits.scale(deposits_, params_.toIndex); - uint256 toBucketDeposit = Maths.wmul(toBucketScale, unscaledToBucketDeposit); - vars.toBucketPrice = _priceAt(params_.toIndex); + vars.toBucketUnscaledDeposit = Deposits.unscaledValueAt(deposits_, params_.toIndex); + vars.toBucketScale = Deposits.scale(deposits_, params_.toIndex); + vars.toBucketDeposit = Maths.wmul(vars.toBucketUnscaledDeposit, vars.toBucketScale); + toBucketLPs_ = Buckets.quoteTokensToLPs( toBucket.collateral, toBucket.lps, - toBucketDeposit, + vars.toBucketDeposit, vars.amountToMove, vars.toBucketPrice ); - Deposits.unscaledAdd(deposits_, params_.toIndex, Maths.wdiv(vars.amountToMove, toBucketScale)); + Deposits.unscaledAdd(deposits_, params_.toIndex, Maths.wdiv(vars.amountToMove, vars.toBucketScale)); lup_ = _lup(deposits_, poolState_.debt); vars.htp = Maths.wmul(params_.thresholdPrice, poolState_.inflator); // check loan book's htp against new lup, revert if move drives LUP below HTP - if (params_.fromIndex < params_.toIndex) if(vars.htp > lup_) revert LUPBelowHTP(); + if (params_.fromIndex < params_.toIndex && vars.htp > lup_) revert LUPBelowHTP(); - // update lender LPs balance in from bucket - fromBucketLender.lps -= fromBucketRedeemedLPs_; + // update lender and bucket LPs balance in from bucket + vars.fromBucketRemainingLPs = vars.fromBucketLPs - fromBucketRedeemedLPs_; - // update lender LPs balance and deposit time in target bucket - Lender storage toBucketLender = toBucket.lenders[msg.sender]; + if (vars.fromBucketCollateral == 0 && vars.fromBucketRemainingDeposit == 0 && vars.fromBucketRemainingLPs != 0) { + emit BucketBankruptcy(params_.fromIndex, vars.fromBucketRemainingLPs); + fromBucket.lps = 0; + fromBucket.bankruptcyTime = block.timestamp; + } else { + // update lender and bucket LPs balance + fromBucketLender.lps -= fromBucketRedeemedLPs_; + + fromBucket.lps = vars.fromBucketRemainingLPs; + } - if (vars.toBucketBankruptcyTime >= toBucketLender.depositTime) toBucketLender.lps = toBucketLPs_; - else toBucketLender.lps += toBucketLPs_; + // update lender and bucket LPs balance in target bucket + Lender storage toBucketLender = toBucket.lenders[msg.sender]; + if (vars.toBucketBankruptcyTime >= toBucketLender.depositTime) { + toBucketLender.lps = toBucketLPs_; + } else { + toBucketLender.lps += toBucketLPs_; + } // set deposit time to the greater of the lender's from bucket and the target bucket's last bankruptcy timestamp + 1 so deposit won't get invalidated toBucketLender.depositTime = Maths.max(vars.fromBucketDepositTime, vars.toBucketBankruptcyTime + 1); - // update buckets LPs balance - fromBucket.lps -= fromBucketRedeemedLPs_; - toBucket.lps += toBucketLPs_; + // update bucket LPs balance + toBucket.lps += toBucketLPs_; emit MoveQuoteToken( msg.sender, @@ -320,7 +344,7 @@ library LenderActions { RemoveDepositParams memory removeParams; - if (bucket.bankruptcyTime < lender.depositTime) removeParams.lpConstraint = lender.lps; + if (bucket.bankruptcyTime < depositTime) removeParams.lpConstraint = lender.lps; if (removeParams.lpConstraint == 0) revert NoClaim(); // revert if no LP to claim @@ -331,8 +355,8 @@ library LenderActions { removeParams.index = params_.index; removeParams.dustLimit = poolState_.quoteDustLimit; - uint256 unscaledRemaining; - (removedAmount_, redeemedLPs_, unscaledRemaining) = _removeMaxDeposit( + uint256 scaledRemaining; + (removedAmount_, redeemedLPs_, scaledRemaining) = _removeMaxDeposit( deposits_, removeParams ); @@ -351,16 +375,16 @@ library LenderActions { // check loan book's htp against new lup if (htp > lup_) revert LUPBelowHTP(); - // update lender and bucket LPs balances - lender.lps -= redeemedLPs_; - uint256 lpsRemaining = removeParams.bucketLPs - redeemedLPs_; - if (removeParams.bucketCollateral == 0 && unscaledRemaining == 0 && lpsRemaining != 0) { + if (removeParams.bucketCollateral == 0 && scaledRemaining == 0 && lpsRemaining != 0) { emit BucketBankruptcy(params_.index, lpsRemaining); bucket.lps = 0; bucket.bankruptcyTime = block.timestamp; } else { + // update lender and bucket LPs balances + lender.lps -= redeemedLPs_; + bucket.lps = lpsRemaining; } @@ -375,6 +399,8 @@ library LenderActions { * @dev reverts on: * - not enough collateral InsufficientCollateral() * - insufficient LPs InsufficientLPs() + * @dev emit events: + * - BucketBankruptcy */ function removeCollateral( mapping(uint256 => Bucket) storage buckets_, @@ -388,13 +414,14 @@ library LenderActions { if (amount_ > bucketCollateral) revert InsufficientCollateral(); - uint256 bucketPrice = _priceAt(index_); - uint256 bucketLPs = bucket.lps; + uint256 bucketPrice = _priceAt(index_); + uint256 bucketLPs = bucket.lps; + uint256 bucketDeposit = Deposits.valueAt(deposits_, index_); lpAmount_ = Buckets.collateralToLPs( bucketCollateral, bucketLPs, - Deposits.valueAt(deposits_, index_), + bucketDeposit, amount_, bucketPrice ); @@ -405,12 +432,27 @@ library LenderActions { if (bucket.bankruptcyTime < lender.depositTime) lenderLpBalance = lender.lps; if (lenderLpBalance == 0 || lpAmount_ > lenderLpBalance) revert InsufficientLPs(); - // update lender LPs balance - lender.lps -= lpAmount_; - // update bucket LPs and collateral balance - bucket.lps -= Maths.min(bucketLPs, lpAmount_); - bucket.collateral -= Maths.min(bucketCollateral, amount_); + bucketLPs -= lpAmount_; + + // If clearing out the bucket collateral, ensure it's zeroed out + if (bucketLPs == 0 && bucketDeposit == 0) { + amount_ = bucketCollateral; + } + + bucketCollateral -= Maths.min(bucketCollateral, amount_); + bucket.collateral = bucketCollateral; + + if (bucketCollateral == 0 && bucketDeposit == 0 && bucketLPs != 0) { + emit BucketBankruptcy(index_, bucketLPs); + bucket.lps = 0; + bucket.bankruptcyTime = block.timestamp; + } else { + // update lender LPs balance + lender.lps -= lpAmount_; + + bucket.lps = bucketLPs; + } } /** @@ -507,7 +549,7 @@ library LenderActions { * - invalid index InvalidIndex() * - no allowance NoAllowance() * @dev emit events: - * - TransferLPTokens + * - TransferLPs */ function transferLPs( mapping(uint256 => Bucket) storage buckets_, @@ -516,6 +558,9 @@ library LenderActions { address newOwner_, uint256[] calldata indexes_ ) external { + // revert if new owner address is the same as old owner address + if (owner_ == newOwner_) revert TransferToSameOwner(); + uint256 indexesLength = indexes_.length; uint256 tokensTransferred; @@ -539,7 +584,7 @@ library LenderActions { delete allowances_[owner_][newOwner_][index]; // delete allowance - // move lp tokens to the new owner address + // move lps to the new owner address Lender storage newLender = bucket.lenders[newOwner_]; newLender.lps += transferAmount; @@ -554,7 +599,7 @@ library LenderActions { unchecked { ++i; } } - emit TransferLPTokens(owner_, newOwner_, indexes_, tokensTransferred); + emit TransferLPs(owner_, newOwner_, indexes_, tokensTransferred); } /**************************/ @@ -600,9 +645,13 @@ library LenderActions { collateralAmount_ = Maths.min(maxAmount_, bucketCollateral); // determine how much LP would be required to remove the requested amount - uint256 collateralValue = Maths.wmul(bucketPrice, bucketCollateral); - uint256 lpsForAllCollateral = Maths.rmul(bucketLPs, Maths.wwdivr(collateralValue, collateralValue + bucketDeposit)); - uint256 requiredLPs = Maths.rmul(lpsForAllCollateral, Maths.wwdivr(collateralAmount_, bucketCollateral)); + uint256 requiredLPs = Buckets.collateralToLPs( + bucketCollateral, + bucketLPs, + bucketDeposit, + collateralAmount_, + bucketPrice + ); // limit withdrawal by the lender's LPB if (requiredLPs <= lenderLpBalance) { @@ -610,21 +659,28 @@ library LenderActions { lpAmount_ = requiredLPs; } else { lpAmount_ = lenderLpBalance; - collateralAmount_ = Maths.wmul(Maths.rrdivw(lenderLpBalance,lpsForAllCollateral), bucketCollateral); + collateralAmount_ = Maths.wmul(Maths.wdiv(lenderLpBalance, requiredLPs), collateralAmount_); } - // update lender LPs balance - lender.lps -= lpAmount_; - // update bucket LPs and collateral balance - bucketLPs -= Maths.min(bucketLPs, lpAmount_); + bucketLPs -= Maths.min(bucketLPs, lpAmount_); + + // If clearing out the bucket collateral, ensure it's zeroed out + if (bucketLPs == 0 && bucketDeposit == 0) { + collateralAmount_ = bucketCollateral; + } + bucketCollateral -= Maths.min(bucketCollateral, collateralAmount_); - bucket.collateral = bucketCollateral; + bucket.collateral = bucketCollateral; + if (bucketCollateral == 0 && bucketDeposit == 0 && bucketLPs != 0) { emit BucketBankruptcy(index_, bucketLPs); bucket.lps = 0; bucket.bankruptcyTime = block.timestamp; } else { + // update lender LPs balance + lender.lps -= lpAmount_; + bucket.lps = bucketLPs; } } @@ -635,69 +691,61 @@ library LenderActions { * @dev write state: * - Deposits.unscaledRemove (remove amount in Fenwick tree, from index): * - update values array state - * @return removedAmount_ Amount of scaled deposit removed. - * @return redeemedLPs_ Amount of bucket LPs corresponding for calculated unscaled deposit amount. + * @return removedAmount_ Amount of scaled deposit removed. + * @return redeemedLPs_ Amount of bucket LPs corresponding for calculated scaled deposit amount. + * @return scaledRemaining_ Amount of scaled deposit remaining. */ function _removeMaxDeposit( DepositsState storage deposits_, RemoveDepositParams memory params_ - ) internal returns (uint256 removedAmount_, uint256 redeemedLPs_, uint256 unscaledRemaining_) { + ) internal returns (uint256 removedAmount_, uint256 redeemedLPs_, uint256 scaledRemaining_) { uint256 unscaledDepositAvailable = Deposits.unscaledValueAt(deposits_, params_.index); if (unscaledDepositAvailable == 0) revert InsufficientLiquidity(); // revert if there's no liquidity available to remove uint256 depositScale = Deposits.scale(deposits_, params_.index); - uint256 unscaledExchangeRate = Buckets.getUnscaledExchangeRate( + uint256 scaledDepositAvailable = Maths.wmul(unscaledDepositAvailable, depositScale); + + uint256 exchangeRate = Buckets.getExchangeRate( params_.bucketCollateral, params_.bucketLPs, - unscaledDepositAvailable, - depositScale, + scaledDepositAvailable, params_.price ); // Below is pseudocode explaining the logic behind finding the constrained amount of deposit and LPB - // unscaledRemovedAmount is constrained by the de-scaled maxAmount(in QT), the unscaledDeposit constraint, and - // the lender LPB exchange rate in unscaled deposit-to-LPB for the bucket: - // unscaledRemovedAmount = min ( maxAmount_/scale, unscaledDeposit, lenderLPsBalance*unscaledExchangeRate) - // redeemedLPs_ = min ( maxAmount_/(unscaledExchangeRate*scale), unscaledDeposit/unscaledExchangeRate, lenderLPsBalance) + // scaledRemovedAmount is constrained by the scaled maxAmount(in QT), the scaledDeposit constraint, and + // the lender LPB exchange rate in scaled deposit-to-LPB for the bucket: + // scaledRemovedAmount = min ( maxAmount_, scaledDeposit, lenderLPsBalance*exchangeRate) + // redeemedLPs_ = min ( maxAmount_/scaledExchangeRate, scaledDeposit/exchangeRate, lenderLPsBalance) - uint256 unscaledRemovedAmount; - uint256 unscaledLpConstraint = Maths.rmul(params_.lpConstraint, unscaledExchangeRate); + uint256 scaledLpConstraint = Maths.wmul(params_.lpConstraint, exchangeRate); if ( - params_.depositConstraint < Maths.wmul(unscaledDepositAvailable, depositScale) && - Maths.wwdivr(params_.depositConstraint, depositScale) < unscaledLpConstraint + params_.depositConstraint < scaledDepositAvailable && + params_.depositConstraint < scaledLpConstraint ) { // depositConstraint is binding constraint - unscaledRemovedAmount = Maths.wdiv(params_.depositConstraint, depositScale); - redeemedLPs_ = Maths.wrdivr(unscaledRemovedAmount, unscaledExchangeRate); - } else if (Maths.wadToRay(unscaledDepositAvailable) < unscaledLpConstraint) { - // unscaledDeposit is binding constraint - unscaledRemovedAmount = unscaledDepositAvailable; - redeemedLPs_ = Maths.wrdivr(unscaledRemovedAmount, unscaledExchangeRate); + removedAmount_ = params_.depositConstraint; + redeemedLPs_ = Maths.wdiv(removedAmount_, exchangeRate); + } else if (scaledDepositAvailable < scaledLpConstraint) { + // scaledDeposit is binding constraint + removedAmount_ = scaledDepositAvailable; + redeemedLPs_ = Maths.wdiv(removedAmount_, exchangeRate); } else { // redeeming all LPs - redeemedLPs_ = params_.lpConstraint; - unscaledRemovedAmount = Maths.rayToWad(Maths.rmul(redeemedLPs_, unscaledExchangeRate)); + redeemedLPs_ = params_.lpConstraint; + removedAmount_ = Maths.wmul(redeemedLPs_, exchangeRate); } // If clearing out the bucket deposit, ensure it's zeroed out if (redeemedLPs_ == params_.bucketLPs) { - unscaledRemovedAmount = unscaledDepositAvailable; + removedAmount_ = scaledDepositAvailable; } - // calculate the scaled amount removed from deposits - removedAmount_ = Maths.wmul(depositScale, unscaledRemovedAmount); - // calculate amount remaining - unscaledRemaining_ = unscaledDepositAvailable - unscaledRemovedAmount; - uint256 remaining = Maths.wmul(depositScale, unscaledRemaining_); - - // abandon dust amounts upon last withdrawal - if (remaining < params_.dustLimit && redeemedLPs_ == params_.bucketLPs) { - unscaledRemovedAmount = unscaledDepositAvailable; - unscaledRemaining_ = 0; - } + scaledRemaining_ = scaledDepositAvailable - removedAmount_; + uint256 unscaledRemovedAmount = Maths.min(unscaledDepositAvailable, Maths.wdiv(removedAmount_, depositScale)); Deposits.unscaledRemove(deposits_, params_.index, unscaledRemovedAmount); // update FenwickTree } diff --git a/src/libraries/helpers/PoolHelper.sol b/src/libraries/helpers/PoolHelper.sol index c8b279a4f..dcb7a79f7 100644 --- a/src/libraries/helpers/PoolHelper.sol +++ b/src/libraries/helpers/PoolHelper.sol @@ -191,7 +191,7 @@ import { Maths } from '../internal/Maths.sol'; // max collateral to lps uint256 rate = Buckets.getExchangeRate(bucketCollateral_, bucketLPs_, deposit_, bucketPrice_); - collateralAmount_ = Maths.rwdivw(Maths.rmul(lenderLPsBalance_, rate), bucketPrice_); + collateralAmount_ = Maths.wdiv(Maths.wmul(lenderLPsBalance_, rate), bucketPrice_); if (collateralAmount_ > bucketCollateral_) { // user is owed more collateral than is available in the bucket @@ -219,7 +219,7 @@ import { Maths } from '../internal/Maths.sol'; ) pure returns (uint256 quoteTokenAmount_) { uint256 rate = Buckets.getExchangeRate(bucketCollateral_, bucketLPs_, deposit_, bucketPrice_); - quoteTokenAmount_ = Maths.rayToWad(Maths.rmul(lenderLPsBalance_, rate)); + quoteTokenAmount_ = Maths.wmul(lenderLPsBalance_, rate); if (quoteTokenAmount_ > deposit_) quoteTokenAmount_ = deposit_; if (quoteTokenAmount_ > maxQuoteToken_) quoteTokenAmount_ = maxQuoteToken_; diff --git a/src/libraries/helpers/RevertsHelper.sol b/src/libraries/helpers/RevertsHelper.sol index 62e9473ec..fe2fc1bc7 100644 --- a/src/libraries/helpers/RevertsHelper.sol +++ b/src/libraries/helpers/RevertsHelper.sol @@ -10,7 +10,7 @@ import { PoolBalancesState } from '../../interfaces/pool/commons/IPoolState.sol'; -import { _minDebtAmount } from './PoolHelper.sol'; +import { _minDebtAmount, _priceAt } from './PoolHelper.sol'; import { Loans } from '../internal/Loans.sol'; import { Deposits } from '../internal/Deposits.sol'; @@ -20,7 +20,9 @@ import { Maths } from '../internal/Maths.sol'; error AuctionNotCleared(); error AmountLTMinDebt(); error DustAmountNotExceeded(); + error LimitIndexExceeded(); error RemoveDepositLockedByAuctionDebt(); + error TransactionExpired(); /** * @notice Called by LPB removal functions assess whether or not LPB is locked. @@ -58,6 +60,37 @@ import { Maths } from '../internal/Maths.sol'; } } + /** + * @notice Check if LUP is at or above index limit provided by borrower. + * @notice Prevents stale transactions and certain MEV manipulations. + * @param newLup_ New LUP as a result of the borrower action. + * @param limitIndex_ Limit price index provided by user creating the TX. + */ + function _revertIfLupDroppedBelowLimit( + uint256 newLup_, + uint256 limitIndex_ + ) pure { + if (newLup_ < _priceAt(limitIndex_)) revert LimitIndexExceeded(); + } + + /** + * @notice Check if expiration provided by user has met or exceeded current block height timestamp. + * @notice Prevents stale transactions interacting with the pool at potentially unfavorable prices. + * @param expiry_ Expiration provided by user when creating the TX. + */ + function _revertOnExpiry( + uint256 expiry_ + ) view { + if (block.timestamp >= expiry_) revert TransactionExpired(); + } + + /** + * @notice Called when borrower debt changes, ensuring minimum debt rules are honored. + * @param loans_ Loans heap, used to determine loan count. + * @param poolDebt_ Total pool debt, used to calculate average debt. + * @param borrowerDebt_ New debt for the borrower, assuming the current transaction succeeds. + * @param quoteDust_ Smallest amount of quote token when can be transferred, determined by token scale. + */ function _revertOnMinDebt( LoansState storage loans_, uint256 poolDebt_, @@ -65,11 +98,9 @@ import { Maths } from '../internal/Maths.sol'; uint256 quoteDust_ ) view { if (borrowerDebt_ != 0) { + if (borrowerDebt_ < quoteDust_) revert DustAmountNotExceeded(); uint256 loansCount = Loans.noOfLoans(loans_); - if (loansCount >= 10) { + if (loansCount >= 10) if (borrowerDebt_ < _minDebtAmount(poolDebt_, loansCount)) revert AmountLTMinDebt(); - } else { - if (borrowerDebt_ < quoteDust_) revert DustAmountNotExceeded(); - } } } diff --git a/src/libraries/internal/Buckets.sol b/src/libraries/internal/Buckets.sol index a192d9305..8075f9307 100644 --- a/src/libraries/internal/Buckets.sol +++ b/src/libraries/internal/Buckets.sol @@ -107,7 +107,7 @@ library Buckets { ) internal pure returns (uint256 lps_) { uint256 rate = getExchangeRate(bucketCollateral_, bucketLPs_, deposit_, bucketPrice_); - lps_ = (collateral_ * bucketPrice_ * 1e18 + rate / 2) / rate; + lps_ = Maths.wdiv(Maths.wmul(collateral_, bucketPrice_), rate); } /** @@ -126,8 +126,8 @@ library Buckets { uint256 quoteTokens_, uint256 bucketPrice_ ) internal pure returns (uint256) { - return Maths.rdiv( - Maths.wadToRay(quoteTokens_), + return Maths.wdiv( + quoteTokens_, getExchangeRate(bucketCollateral_, bucketLPs_, deposit_, bucketPrice_) ); } @@ -145,30 +145,7 @@ library Buckets { uint256 bucketDeposit_, uint256 bucketPrice_ ) internal pure returns (uint256) { - return bucketLPs_ == 0 - ? Maths.RAY - : (bucketDeposit_ * 1e18 + bucketPrice_ * bucketCollateral_) * 1e18 / bucketLPs_; - // 10^36 * 1e18 / 10^27 = 10^54 / 10^27 = 10^27 - } - - /** - * @notice Returns the unscaled exchange rate for a given bucket. - * @param bucketCollateral_ Amount of collateral in bucket. - * @param bucketLPs_ Amount of LPs in bucket. - * @param bucketUnscaledDeposit_ The amount of unscaled Fenwick tree amount in bucket. - * @param bucketScale_ Bucket scale factor - * @param bucketPrice_ Bucket's price. - */ - function getUnscaledExchangeRate( - uint256 bucketCollateral_, - uint256 bucketLPs_, - uint256 bucketUnscaledDeposit_, - uint256 bucketScale_, - uint256 bucketPrice_ - ) internal pure returns (uint256) { - return bucketLPs_ == 0 - ? Maths.RAY - : (bucketUnscaledDeposit_ + bucketPrice_ * bucketCollateral_ / bucketScale_ ) * 10**36 / bucketLPs_; - // 10^18 * 1e36 / 10^27 = 10^54 / 10^27 = 10^27 + return bucketLPs_ == 0 ? Maths.WAD : + Maths.wdiv(bucketDeposit_ + Maths.wmul(bucketPrice_, bucketCollateral_), bucketLPs_); } } diff --git a/src/libraries/internal/Loans.sol b/src/libraries/internal/Loans.sol index fefe20cc3..ff1bf1200 100644 --- a/src/libraries/internal/Loans.sol +++ b/src/libraries/internal/Loans.sol @@ -64,14 +64,14 @@ library Loans { * - remove: * - remove loan from loans array * - update borrower in address => borrower mapping - * @param loans_ Holds loan heap data. - * @param borrower_ Borrower struct with borrower details. - * @param borrowerAddress_ Borrower's address to update. - * @param borrowerAccruedDebt_ Borrower's current debt. - * @param poolRate_ Pool's current rate. - * @param lup_ Current LUP. - * @param inAuction_ Whether the loan is in auction or not. - * @param t0NpUpdate_ Whether the neutral price of borrower should be updated or not. + * @param loans_ Holds loan heap data. + * @param borrower_ Borrower struct with borrower details. + * @param borrowerAddress_ Borrower's address to update. + * @param poolDebt_ Pool's current debt. + * @param poolRate_ Pool's current rate. + * @param lup_ Current LUP. + * @param inAuction_ Whether the loan is in auction or not. + * @param t0NpUpdate_ Whether the neutral price of borrower should be updated or not. */ function update( LoansState storage loans_, @@ -79,7 +79,7 @@ library Loans { DepositsState storage deposits_, Borrower memory borrower_, address borrowerAddress_, - uint256 borrowerAccruedDebt_, + uint256 poolDebt_, uint256 poolRate_, uint256 lup_, bool inAuction_, @@ -111,7 +111,7 @@ library Loans { if (t0NpUpdate_) { if (t0ThresholdPrice != 0) { uint256 loansInPool = loans_.loans.length - 1 + auctions_.noOfAuctions; - uint256 curMomp = _priceAt(Deposits.findIndexOfSum(deposits_, Maths.wdiv(borrowerAccruedDebt_, loansInPool * 1e18))); + uint256 curMomp = _priceAt(Deposits.findIndexOfSum(deposits_, Maths.wdiv(poolDebt_, loansInPool * 1e18))); borrower_.t0Np = (1e18 + poolRate_) * curMomp * t0ThresholdPrice / lup_ / 1e18; } else { diff --git a/src/libraries/internal/Maths.sol b/src/libraries/internal/Maths.sol index 75d12c68f..c01f6e059 100644 --- a/src/libraries/internal/Maths.sol +++ b/src/libraries/internal/Maths.sol @@ -9,7 +9,6 @@ pragma solidity 0.8.14; library Maths { uint256 internal constant WAD = 1e18; - uint256 internal constant RAY = 10**27; function wmul(uint256 x, uint256 y) internal pure returns (uint256) { return (x * y + 1e18 / 2) / 1e18; @@ -35,30 +34,6 @@ library Maths { return (x * y + 10**27 / 2) / 10**27; } - function rdiv(uint256 x, uint256 y) internal pure returns (uint256) { - return (x * 10**27 + y / 2) / y; - } - - /** @notice Divides a WAD by a RAY and returns a RAY */ - function wrdivr(uint256 x, uint256 y) internal pure returns (uint256) { - return (x * 1e36 + y / 2) / y; - } - - /** @notice Divides a WAD by a WAD and returns a RAY */ - function wwdivr(uint256 x, uint256 y) internal pure returns (uint256) { - return (x * 1e27 + y / 2) / y; - } - - /** @notice Divides a RAY by another RAY and returns a WAD */ - function rrdivw(uint256 x, uint256 y) internal pure returns (uint256) { - return (x * 1e18 + y / 2) / y; - } - - /** @notice Divides a RAY by a WAD and returns a WAD */ - function rwdivw(uint256 x, uint256 y) internal pure returns (uint256) { - return (x * 1e9 + y / 2) / y; - } - function rpow(uint256 x, uint256 n) internal pure returns (uint256 z) { z = n % 2 != 0 ? x : 10**27; @@ -71,10 +46,6 @@ library Maths { } } - function wadToRay(uint256 x) internal pure returns (uint256) { - return x * 10**9; - } - function rayToWad(uint256 x) internal pure returns (uint256) { return (x + 10**9 / 2) / 10**9; } 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/brownie/test_scaled_pool.py b/tests/brownie/test_scaled_pool.py index de812d77e..c926480d1 100644 --- a/tests/brownie/test_scaled_pool.py +++ b/tests/brownie/test_scaled_pool.py @@ -7,13 +7,14 @@ def test_quote_deposit_move_remove_scaled( lenders, scaled_pool, + chain, capsys, test_utils ): with test_utils.GasWatcher(["addQuoteToken", "moveQuoteToken", "removeQuoteToken"]): add_txes = [] for i in range(2530, 2550): - tx = scaled_pool.addQuoteToken(100 * 10**18, i, {"from": lenders[0]}) + tx = scaled_pool.addQuoteToken(100 * 10**18, i, chain.time() + 30, {"from": lenders[0]}) add_txes.append(tx) with capsys.disabled(): print("\n==================================") @@ -24,7 +25,7 @@ def test_quote_deposit_move_remove_scaled( move_txes = [] for i in range(2530, 2550): - tx = scaled_pool.moveQuoteToken(100 * 10**18, i, i + 30, {"from": lenders[0]}) + tx = scaled_pool.moveQuoteToken(100 * 10**18, i, i + 30, chain.time() + 30, {"from": lenders[0]}) move_txes.append(tx) with capsys.disabled(): print("\n==================================") @@ -49,14 +50,16 @@ def test_borrow_repay_scaled( lenders, borrowers, scaled_pool, + chain, capsys, test_utils ): with test_utils.GasWatcher(["addQuoteToken", "drawDebt", "repayDebt"]): - scaled_pool.addQuoteToken(100 * 10**18, 2550, {"from": lenders[0]}) - scaled_pool.addQuoteToken(100 * 10**18, 2560, {"from": lenders[0]}) - scaled_pool.addQuoteToken(100 * 10**18, 2570, {"from": lenders[0]}) + expiry = chain.time() + 30 + scaled_pool.addQuoteToken(100 * 10**18, 2550, expiry, {"from": lenders[0]}) + scaled_pool.addQuoteToken(100 * 10**18, 2560, expiry, {"from": lenders[0]}) + scaled_pool.addQuoteToken(100 * 10**18, 2570, expiry, {"from": lenders[0]}) col_txes = [] for i in range(10): @@ -85,11 +88,11 @@ def test_borrow_repay_scaled( print(f"Transaction: {i} | {test_utils.get_usage(txes[i].gas_used)}") repay_txes = [] - tx = scaled_pool.repayDebt(borrowers[0], 110 * 10**18, 0, {"from": borrowers[0]}) + tx = scaled_pool.repayDebt(borrowers[0], 110 * 10**18, 0, borrowers[0], 7388, {"from": borrowers[0]}) repay_txes.append(tx) - tx = scaled_pool.repayDebt(borrowers[0], 110 * 10**18, 0, {"from": borrowers[0]}) + tx = scaled_pool.repayDebt(borrowers[0], 110 * 10**18, 0, borrowers[0], 7388, {"from": borrowers[0]}) repay_txes.append(tx) - tx = scaled_pool.repayDebt(borrowers[0], 50 * 10**18, 0, {"from": borrowers[0]}) + tx = scaled_pool.repayDebt(borrowers[0], 50 * 10**18, 0, borrowers[0], 7388, {"from": borrowers[0]}) repay_txes.append(tx) with capsys.disabled(): print("\n==================================") diff --git a/tests/brownie/test_stable_volatile.py b/tests/brownie/test_stable_volatile.py index e121ba1d4..49f6c1762 100644 --- a/tests/brownie/test_stable_volatile.py +++ b/tests/brownie/test_stable_volatile.py @@ -76,7 +76,7 @@ def borrowers(ajna_protocol, scaled_pool): def pool_helper(ajna_protocol, scaled_pool, lenders, borrowers, test_utils, chain): pool_helper = PoolHelper(ajna_protocol, scaled_pool) # Adds liquidity to an empty pool and draws debt up to a target utilization - add_initial_liquidity(lenders, pool_helper) + add_initial_liquidity(lenders, pool_helper, chain) draw_initial_debt(borrowers, pool_helper, test_utils, chain, target_utilization=GOAL_UTILIZATION) global last_triggered last_triggered = dict.fromkeys(range(0, max(NUM_LENDERS, NUM_BORROWERS)), 0) @@ -84,7 +84,7 @@ def pool_helper(ajna_protocol, scaled_pool, lenders, borrowers, test_utils, chai return pool_helper -def add_initial_liquidity(lenders, pool_helper): +def add_initial_liquidity(lenders, pool_helper, chain): # Lenders 0-9 will be "new to the pool" upon actual testing # TODO: determine this non-arbitrarily deposit_amount = MIN_PARTICIPATION * 10**18 @@ -97,7 +97,7 @@ def add_initial_liquidity(lenders, pool_helper): price_index = price_position + MAX_BUCKET log(f" lender {i} depositing {deposit_amount/1e18} into bucket {price_index} " f"({pool_helper.indexToPrice(price_index) / 1e18:.1f})") - pool_helper.pool.addQuoteToken(deposit_amount, price_index, {"from": lenders[i]}) + pool_helper.pool.addQuoteToken(deposit_amount, price_index, chain.time() + 30, {"from": lenders[i]}) def draw_initial_debt(borrowers, pool_helper, test_utils, chain, target_utilization): @@ -249,7 +249,7 @@ def draw_and_bid(lenders, borrowers, start_from, pool_helper, chain, test_utils, # Add or remove liquidity if user_index < NUM_LENDERS: if random.choice([True, False]): - price = add_quote_token(lenders[user_index], user_index, pool_helper) + price = add_quote_token(lenders[user_index], user_index, pool_helper, chain) if price: buckets_deposited[user_index].add(price) else: @@ -294,7 +294,7 @@ def draw_debt(borrower, borrower_index, pool_helper, test_utils, collateralizati tx = pledge_and_borrow(pool_helper, borrower, borrower_index, collateral_to_deposit, borrow_amount, test_utils) -def add_quote_token(lender, lender_index, pool_helper): +def add_quote_token(lender, lender_index, pool_helper, chain): dai = pool_helper.quoteToken() index_offset = ((lender_index % 6) - 2) * 2 lup_index = pool_helper.lupIndex() @@ -307,7 +307,7 @@ def add_quote_token(lender, lender_index, pool_helper): return None log(f" lender {lender_index:>4} adding {quantity / 10**18:.1f} liquidity at {deposit_price / 10**18:.1f}") - tx = pool_helper.pool.addQuoteToken(quantity, deposit_index, {"from": lender}) + tx = pool_helper.pool.addQuoteToken(quantity, deposit_index, chain.time() + 15, {"from": lender}) return deposit_price @@ -364,7 +364,7 @@ def repay_debt(borrower, borrower_index, pool_helper, test_utils): f"is withdrawing {collateral_deposited/1e18:.1f} collateral") # assert collateral_to_withdraw > 0 repay_amount = int(repay_amount * 1.01) - tx = pool_helper.pool.repayDebt(borrower, repay_amount, collateral_to_withdraw, {"from": borrower}) + tx = pool_helper.pool.repayDebt(borrower, repay_amount, collateral_to_withdraw, borrower, 7388, {"from": borrower}) elif debt == 0: log(f" borrower {borrower_index:>4} has no debt to repay") else: diff --git a/tests/forge/ERC20Pool/ERC20DSTestPlus.sol b/tests/forge/ERC20Pool/ERC20DSTestPlus.sol index 38badd274..0cc630f02 100644 --- a/tests/forge/ERC20Pool/ERC20DSTestPlus.sol +++ b/tests/forge/ERC20Pool/ERC20DSTestPlus.sol @@ -75,8 +75,9 @@ abstract contract ERC20DSTestPlus is DSTestPlus, IERC20PoolEvents { (, uint256 bucketQuote, uint256 bucketCollateral, , ,) = _poolUtils.bucketInfo(address(_pool), bucketIndex); (uint256 lenderLpBalance, ) = _pool.lenderInfo(bucketIndex, lender); - // redeem LP for quote token if available uint256 lpRedeemed; + + // redeem LP for quote token if available if(lenderLpBalance != 0 && bucketQuote != 0) { (, lpRedeemed) = _pool.removeQuoteToken(type(uint256).max, bucketIndex); lenderLpBalance -= lpRedeemed; @@ -88,8 +89,6 @@ abstract contract ERC20DSTestPlus is DSTestPlus, IERC20PoolEvents { lenderLpBalance -= lpRedeemed; } - // confirm the redemption amount returned by removal methods is correct - assertEq(lenderLpBalance, 0); // confirm the user actually has 0 LPB in the bucket (lenderLpBalance, ) = _pool.lenderInfo(bucketIndex, lender); assertEq(lenderLpBalance, 0); @@ -195,7 +194,7 @@ abstract contract ERC20DSTestPlus is DSTestPlus, IERC20PoolEvents { lendersDepositedIndex[from].add(index); bucketsUsed.add(index); - return ERC20Pool(address(_pool)).addCollateral(amount, index); + return ERC20Pool(address(_pool)).addCollateral(amount, index, type(uint256).max); } function _addCollateralWithoutCheckingLP( @@ -218,7 +217,7 @@ abstract contract ERC20DSTestPlus is DSTestPlus, IERC20PoolEvents { lendersDepositedIndex[from].add(index); bucketsUsed.add(index); - return ERC20Pool(address(_pool)).addCollateral(amount, index); + return ERC20Pool(address(_pool)).addCollateral(amount, index, type(uint256).max); } function _borrow( @@ -366,7 +365,7 @@ abstract contract ERC20DSTestPlus is DSTestPlus, IERC20PoolEvents { vm.expectEmit(true, true, false, true); emit RepayDebt(borrower, repaid, 0, newLup); _assertQuoteTokenTransferEvent(from, address(_pool), repaid); - ERC20Pool(address(_pool)).repayDebt(borrower, amount, 0); + ERC20Pool(address(_pool)).repayDebt(borrower, amount, 0, borrower, MAX_FENWICK_INDEX); } function _repayDebt( @@ -394,7 +393,23 @@ abstract contract ERC20DSTestPlus is DSTestPlus, IERC20PoolEvents { _assertCollateralTokenTransferEvent(address(_pool), from, collateralToPull); } - ERC20Pool(address(_pool)).repayDebt(borrower, amountToRepay, collateralToPull); + ERC20Pool(address(_pool)).repayDebt(borrower, amountToRepay, collateralToPull, borrower, MAX_FENWICK_INDEX); + } + + function _repayDebtAndPullToRecipient( + address from, + address borrower, + address recipient, + uint256 amountToRepay, + uint256 amountRepaid, + uint256 collateralToPull, + uint256 newLup + ) internal { + changePrank(from); + vm.expectEmit(true, true, false, true); + emit RepayDebt(borrower, amountRepaid, collateralToPull, newLup); + _assertCollateralTokenTransferEvent(address(_pool), recipient, collateralToPull); + ERC20Pool(address(_pool)).repayDebt(borrower, amountToRepay, collateralToPull, recipient, MAX_FENWICK_INDEX); } function _repayDebtNoLupCheck( @@ -407,7 +422,7 @@ abstract contract ERC20DSTestPlus is DSTestPlus, IERC20PoolEvents { _repayDebt(from, borrower, amountToRepay, amountRepaid, collateralToPull, 0); } - function _transferLpTokens( + function _transferLPs( address operator, address from, address to, @@ -416,7 +431,7 @@ abstract contract ERC20DSTestPlus is DSTestPlus, IERC20PoolEvents { ) internal { changePrank(operator); vm.expectEmit(true, true, true, true); - emit TransferLPTokens(from, to, indexes, lpBalance); + emit TransferLPs(from, to, indexes, lpBalance); _pool.transferLPs(from, to, indexes); for(uint256 i = 0; i < indexes.length ;i++ ){ @@ -439,7 +454,7 @@ abstract contract ERC20DSTestPlus is DSTestPlus, IERC20PoolEvents { ) internal { changePrank(from); vm.expectRevert(abi.encodeWithSignature('BucketBankruptcyBlock()')); - ERC20Pool(address(_pool)).addCollateral(amount, index); + ERC20Pool(address(_pool)).addCollateral(amount, index, type(uint256).max); } function _assertAddCollateralAtIndex0Revert( @@ -448,7 +463,7 @@ abstract contract ERC20DSTestPlus is DSTestPlus, IERC20PoolEvents { ) internal { changePrank(from); vm.expectRevert(IPoolErrors.InvalidIndex.selector); - ERC20Pool(address(_pool)).addCollateral(amount, 0); + ERC20Pool(address(_pool)).addCollateral(amount, 0, type(uint256).max); } function _assertAddCollateralDustRevert( @@ -458,7 +473,18 @@ abstract contract ERC20DSTestPlus is DSTestPlus, IERC20PoolEvents { ) internal { changePrank(from); vm.expectRevert(IPoolErrors.DustAmountNotExceeded.selector); - ERC20Pool(address(_pool)).addCollateral(amount, index); + ERC20Pool(address(_pool)).addCollateral(amount, index, type(uint256).max); + } + + function _assertAddCollateralExpiredRevert( + address from, + uint256 amount, + uint256 index, + uint256 expiry + ) internal { + changePrank(from); + vm.expectRevert(IPoolErrors.TransactionExpired.selector); + ERC20Pool(address(_pool)).addCollateral(amount, index, expiry); } function _assertDeployWith0xAddressRevert( @@ -497,7 +523,17 @@ abstract contract ERC20DSTestPlus is DSTestPlus, IERC20PoolEvents { ) internal { changePrank(from); vm.expectRevert(IPoolErrors.InsufficientCollateral.selector); - ERC20Pool(address(_pool)).repayDebt(from, 0, amount); + ERC20Pool(address(_pool)).repayDebt(from, 0, amount, from, MAX_FENWICK_INDEX); + } + + function _assertPullLimitIndexRevert( + address from, + uint256 amount, + uint256 indexLimit + ) internal { + changePrank(from); + vm.expectRevert(IPoolErrors.LimitIndexExceeded.selector); + ERC20Pool(address(_pool)).repayDebt(from, 0, amount, from, indexLimit); } function _assertRepayNoDebtRevert( @@ -507,7 +543,7 @@ abstract contract ERC20DSTestPlus is DSTestPlus, IERC20PoolEvents { ) internal { changePrank(from); vm.expectRevert(IPoolErrors.NoDebt.selector); - ERC20Pool(address(_pool)).repayDebt(borrower, amount, 0); + ERC20Pool(address(_pool)).repayDebt(borrower, amount, 0, borrower, MAX_FENWICK_INDEX); } function _assertPullBorrowerNotSenderRevert( @@ -517,7 +553,7 @@ abstract contract ERC20DSTestPlus is DSTestPlus, IERC20PoolEvents { ) internal { changePrank(from); vm.expectRevert(IPoolErrors.BorrowerNotSender.selector); - ERC20Pool(address(_pool)).repayDebt(borrower, 0, amount); + ERC20Pool(address(_pool)).repayDebt(borrower, 0, amount, borrower, MAX_FENWICK_INDEX); } function _assertRepayMinDebtRevert( @@ -527,7 +563,7 @@ abstract contract ERC20DSTestPlus is DSTestPlus, IERC20PoolEvents { ) internal { changePrank(from); vm.expectRevert(IPoolErrors.AmountLTMinDebt.selector); - ERC20Pool(address(_pool)).repayDebt(borrower, amount, 0); + ERC20Pool(address(_pool)).repayDebt(borrower, amount, 0, borrower, MAX_FENWICK_INDEX); } function _assertRemoveAllCollateralNoClaimRevert( @@ -580,6 +616,17 @@ abstract contract ERC20DSTestPlus is DSTestPlus, IERC20PoolEvents { _pool.transferLPs(from, to, indexes); } + function _assertTransferToSameOwnerRevert( + address operator, + address from, + address to, + uint256[] memory indexes + ) internal { + changePrank(operator); + vm.expectRevert(IPoolErrors.TransferToSameOwner.selector); + _pool.transferLPs(from, to, indexes); + } + function _assertDepositLockedByAuctionDebtRevert( address operator, uint256 amount, @@ -596,7 +643,7 @@ abstract contract ERC20DSTestPlus is DSTestPlus, IERC20PoolEvents { uint256 indexLimit ) internal override { changePrank(from); - vm.expectRevert(IPoolErrors.BorrowerUnderCollateralized.selector); + vm.expectRevert(IPoolErrors.AuctionActive.selector); ERC20Pool(address(_pool)).drawDebt(from, amount, indexLimit, 0); } @@ -617,7 +664,7 @@ abstract contract ERC20DSTestPlus is DSTestPlus, IERC20PoolEvents { uint256 indexLimit ) internal override { changePrank(from); - vm.expectRevert(IPoolErrors.LimitIndexReached.selector); + vm.expectRevert(IPoolErrors.LimitIndexExceeded.selector); ERC20Pool(address(_pool)).drawDebt(from, amount, indexLimit, 0); } diff --git a/tests/forge/ERC20Pool/ERC20FlashloanCollateral.t.sol b/tests/forge/ERC20Pool/ERC20FlashloanCollateral.t.sol deleted file mode 100644 index 25d971830..000000000 --- a/tests/forge/ERC20Pool/ERC20FlashloanCollateral.t.sol +++ /dev/null @@ -1,141 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.14; - -import { ERC20HelperContract } from './ERC20DSTestPlus.sol'; -import { FlashloanBorrower, SomeDefiStrategy } from '../utils/FlashloanBorrower.sol'; - -import 'src/libraries/helpers/PoolHelper.sol'; -import 'src/ERC20Pool.sol'; - -import { IPoolErrors } from 'src/interfaces/pool/IPool.sol'; - -contract ERC20PoolFlashloanTest is ERC20HelperContract { - address internal _borrower; - address internal _lender; - uint internal _bucketId; - uint internal _bucketPrice; - - function setUp() external { - _lender = makeAddr("lender"); - _borrower = makeAddr("borrower"); - - _mintQuoteAndApproveTokens(_lender, 100_000 * 1e18); - _mintQuoteAndApproveTokens(_borrower, 5_000 * 1e18); - _mintCollateralAndApproveTokens(_borrower, 100 * 1e18); - - // lender adds liquidity - _bucketPrice = 502.433988063349232760 * 1e18; - _bucketId = _indexOf(_bucketPrice); - assertEq(_bucketId, 2909); - _addInitialLiquidity( - { - from: _lender, - amount: 100_000 * 1e18, - index: _bucketId - } - ); - - // borrower draws debt - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - amount: 100 * 1e18 - } - ); - _borrow( - { - from: _borrower, - amount: 25_000 * 1e18, - indexLimit: _bucketId, - newLup: _bucketPrice - } - ); - (uint256 poolDebt,,) = _pool.debtInfo(); - assertEq(poolDebt, 25_024.038461538461550000 * 1e18); - } - - function testCollateralFlashloan() external tearDown { - skip(1 days); - uint256 loanAmount = 100 * 1e18; - assertEq(_pool.maxFlashLoan(address(_collateral)), loanAmount); - - // Create an example defi strategy - SomeDefiStrategy strategy = new SomeDefiStrategy(_collateral); - deal(address(_collateral), address(strategy), 10 * 1e18); - - // Create a flashloan borrower contract which interacts with the strategy - bytes memory strategyCalldata = abi.encodeWithSignature("makeMoney(uint256)", loanAmount); - FlashloanBorrower flasher = new FlashloanBorrower(address(strategy), strategyCalldata); - - // Run the token approvals - changePrank(address(flasher)); - _collateral.approve(address(_pool), loanAmount); - _collateral.approve(address(strategy), loanAmount); - - // Use a flashloan to interact with the strategy - assertEq(_collateral.balanceOf(address(flasher)), 0); - assertTrue(!flasher.callbackInvoked()); - _pool.flashLoan(flasher, address(_collateral), loanAmount, new bytes(0)); - assertTrue(flasher.callbackInvoked()); - assertEq(_collateral.balanceOf(address(flasher)), 3.5 * 1e18); - } - - function testFlashloanFee() external tearDown { - uint256 loanAmount = 100 * 1e18; - - // Ensure there is no fee for quote token - uint256 fee = _pool.flashFee(address(_quote), loanAmount); - assertEq(fee, 0); - - // Ensure there is no fee for collateral - fee = _pool.flashFee(address(_collateral), loanAmount); - assertEq(fee, 0); - - // Ensure fee reverts for a random address which isn't a token - _assertFlashloanFeeRevertsForToken(makeAddr("nobody"), loanAmount); - } - - function testMaxFlashloan() external tearDown { - assertEq(_pool.maxFlashLoan(_pool.quoteTokenAddress()), 75_000 * 1e18); - assertEq(_pool.maxFlashLoan(_pool.collateralAddress()), 100 * 1e18); - assertEq(_pool.maxFlashLoan(makeAddr("nobody")), 0); - } - - function testCannotFlashloanMoreCollateralThanAvailable() external tearDown { - FlashloanBorrower flasher = new FlashloanBorrower(address(0), new bytes(0)); - - // Cannot flashloan less than pool size but more than available quote token - _assertFlashloanTooLargeRevert(flasher, _pool.quoteTokenAddress(), 90_000 * 1e18); - - // Cannot flashloan more collateral than pledged - _assertFlashloanTooLargeRevert(flasher, _pool.collateralAddress(), 150 * 1e18); - } - - function testCannotFlashloanNonToken() external tearDown { - FlashloanBorrower flasher = new FlashloanBorrower(address(0), new bytes(0)); - - // Cannot flashloan a random address which isn't a token - _assertFlashloanUnavailableForToken(flasher, makeAddr("nobody"), 1); - } - - function testCallbackFailure() external tearDown { - uint256 loanAmount = 100 * 1e18; - - // Create an example defi strategy - SomeDefiStrategy strategy = new SomeDefiStrategy(_collateral); - - // Create a flashloan borrower contract which invokes a non-existant method on the strategy - bytes memory strategyCalldata = abi.encodeWithSignature("missing()"); - FlashloanBorrower flasher = new FlashloanBorrower(address(strategy), strategyCalldata); - - // Run approvals - changePrank(address(flasher)); - _quote.approve(address(_pool), loanAmount); - - // Make a failed attempt to interact with the strategy - vm.expectRevert(IPoolErrors.FlashloanCallbackFailed.selector); - _pool.flashLoan(flasher, address(_collateral), loanAmount, new bytes(0)); - assertFalse(flasher.callbackInvoked()); - } -} \ No newline at end of file diff --git a/tests/forge/ERC20Pool/ERC20PoolBorrow.t.sol b/tests/forge/ERC20Pool/ERC20PoolBorrow.t.sol index b30e6ea11..cfa46f2bc 100644 --- a/tests/forge/ERC20Pool/ERC20PoolBorrow.t.sol +++ b/tests/forge/ERC20Pool/ERC20PoolBorrow.t.sol @@ -33,52 +33,42 @@ contract ERC20PoolBorrowTest is ERC20HelperContract { _mintQuoteAndApproveTokens(_lender, 200_000 * 1e18); _mintQuoteAndApproveTokens(_lender1, 200_000 * 1e18); - // lender deposits 10000 DAI in 5 buckets each - _addLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: highest, - lpAward: 10_000 * 1e27, - newLup: MAX_PRICE - } - ); - _addLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: high, - lpAward: 10_000 * 1e27, - newLup: MAX_PRICE - } - ); - _addLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: med, - lpAward: 10_000 * 1e27, - newLup: MAX_PRICE - } - ); - _addLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: low, - lpAward: 10_000 * 1e27, - newLup: MAX_PRICE - } - ); - _addLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: lowest, - lpAward: 10_000 * 1e27, - newLup: MAX_PRICE - } - ); + // lender deposits 10000 quote in 5 buckets each + _addLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: highest, + lpAward: 10_000 * 1e18, + newLup: MAX_PRICE + }); + _addLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: high, + lpAward: 10_000 * 1e18, + newLup: MAX_PRICE + }); + _addLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: med, + lpAward: 10_000 * 1e18, + newLup: MAX_PRICE + }); + _addLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: low, + lpAward: 10_000 * 1e18, + newLup: MAX_PRICE + }); + _addLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: lowest, + lpAward: 10_000 * 1e18, + newLup: MAX_PRICE + }); _assertPool( PoolParams({ @@ -135,103 +125,81 @@ contract ERC20PoolBorrowTest is ERC20HelperContract { assertEq(_quote.balanceOf(address(_pool)), 29_000 * 1e18); assertEq(_quote.balanceOf(_lender), 150_000 * 1e18); - _assertLenderLpBalance( - { - lender: _lender, - index: highest, - lpBalance: 10_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: high, - lpBalance: 10_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: med, - lpBalance: 10_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: low, - lpBalance: 10_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: lowest, - lpBalance: 10_000 * 1e27, - depositTime: _startTime - } - ); + _assertLenderLpBalance({ + lender: _lender, + index: highest, + lpBalance: 10_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: _lender, + index: high, + lpBalance: 10_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: _lender, + index: med, + lpBalance: 10_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: _lender, + index: low, + lpBalance: 10_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: _lender, + index: lowest, + lpBalance: 10_000 * 1e18, + depositTime: _startTime + }); // check buckets - _assertBucket( - { - index: highest, - lpBalance: 10_000 * 1e27, - collateral: 0, - deposit: 10_000 * 1e18, - exchangeRate: 1 * 1e27 - } - ); - _assertBucket( - { - index: high, - lpBalance: 10_000 * 1e27, - collateral: 0, - deposit: 10_000 * 1e18, - exchangeRate: 1 * 1e27 - } - ); - _assertBucket( - { - index: med, - lpBalance: 10_000 * 1e27, - collateral: 0, - deposit: 10_000 * 1e18, - exchangeRate: 1 * 1e27 - } - ); - _assertBucket( - { - index: low, - lpBalance: 10_000 * 1e27, - collateral: 0, - deposit: 10_000 * 1e18, - exchangeRate: 1 * 1e27 - } - ); - _assertBucket( - { - index: lowest, - lpBalance: 10_000 * 1e27, - collateral: 0, - deposit: 10_000 * 1e18, - exchangeRate: 1 * 1e27 - } - ); + _assertBucket({ + index: highest, + lpBalance: 10_000 * 1e18, + collateral: 0, + deposit: 10_000 * 1e18, + exchangeRate: 1 * 1e18 + }); + _assertBucket({ + index: high, + lpBalance: 10_000 * 1e18, + collateral: 0, + deposit: 10_000 * 1e18, + exchangeRate: 1 * 1e18 + }); + _assertBucket({ + index: med, + lpBalance: 10_000 * 1e18, + collateral: 0, + deposit: 10_000 * 1e18, + exchangeRate: 1 * 1e18 + }); + _assertBucket({ + index: low, + lpBalance: 10_000 * 1e18, + collateral: 0, + deposit: 10_000 * 1e18, + exchangeRate: 1 * 1e18 + }); + _assertBucket({ + index: lowest, + lpBalance: 10_000 * 1e18, + collateral: 0, + deposit: 10_000 * 1e18, + exchangeRate: 1 * 1e18 + }); - // borrow 19_000 DAI - _borrow( - { - from: _borrower, - amount: 19_000 * 1e18, - indexLimit: 3_500, - newLup: 2_951.419442869698640451 * 1e18 - } - ); + // borrow 19_000 quote + _borrow({ + from: _borrower, + amount: 19_000 * 1e18, + indexLimit: 3_500, + newLup: 2_951.419442869698640451 * 1e18 + }); _assertPool( PoolParams({ @@ -250,6 +218,7 @@ contract ERC20PoolBorrowTest is ERC20HelperContract { interestRateUpdate: _startTime }) ); + // check balances assertEq(_quote.balanceOf(address(_pool)), 10_000 * 1e18); assertEq(_quote.balanceOf(_lender), 150_000 * 1e18); @@ -319,15 +288,13 @@ contract ERC20PoolBorrowTest is ERC20HelperContract { assertEq(_quote.balanceOf(address(_pool)), 50_038.461538461538480000 * 1e18); assertEq(_quote.balanceOf(_lender), 150_000 * 1e18); - // borrow 8_000 DAI - _borrow( - { - from: _borrower, - amount: 8_000 * 1e18, - indexLimit: 3_500, - newLup: 3_010.892022197881557845 * 1e18 - } - ); + // borrow 8_000 quote + _borrow({ + from: _borrower, + amount: 8_000 * 1e18, + indexLimit: 3_500, + newLup: 3_010.892022197881557845 * 1e18 + }); _assertPool( PoolParams({ @@ -350,7 +317,9 @@ contract ERC20PoolBorrowTest is ERC20HelperContract { function testPoolBorrowerInterestAccumulation() external tearDown { (uint256 liquidityAdded, , , , ) = _poolUtils.poolLoansInfo(address(_pool)); + skip(10 days); + _drawDebt({ from: _borrower, borrower: _borrower, @@ -361,6 +330,7 @@ contract ERC20PoolBorrowTest is ERC20HelperContract { }); uint256 expectedDebt = 21_051.890446235135648008 * 1e18; + _assertPool( PoolParams({ htp: 420.403846153846154040 * 1e18, @@ -378,26 +348,24 @@ contract ERC20PoolBorrowTest is ERC20HelperContract { interestRateUpdate: _startTime + 10 days }) ); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: expectedDebt, - borrowerCollateral: 50 * 1e18, - borrowert0Np: 441.424038461538461742 * 1e18, - borrowerCollateralization: 7.080141877038845214 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: expectedDebt, + borrowerCollateral: 50 * 1e18, + borrowert0Np: 441.424038461538461742 * 1e18, + borrowerCollateralization: 7.080141877038845214 * 1e18 + }); skip(10 days); - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - amount: 10 * 1e18 - } - ); + + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + amount: 10 * 1e18 + }); expectedDebt = 21_083.636385101213387311 * 1e18; + _assertPool( PoolParams({ htp: 352.454532537342231182 * 1e18, @@ -415,18 +383,17 @@ contract ERC20PoolBorrowTest is ERC20HelperContract { interestRateUpdate: _startTime + 20 days }) ); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: expectedDebt, - borrowerCollateral: 60 * 1e18, - borrowert0Np: 441.424038461538461742 * 1e18, - borrowerCollateralization: 8.483377444958217435 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: expectedDebt, + borrowerCollateral: 60 * 1e18, + borrowert0Np: 441.424038461538461742 * 1e18, + borrowerCollateralization: 8.483377444958217435 * 1e18 + }); _assertLenderInterest(liquidityAdded, 55.509493137959600000 * 1e18); skip(10 days); + _repayDebtNoLupCheck({ from: _borrower, borrower: _borrower, @@ -436,6 +403,7 @@ contract ERC20PoolBorrowTest is ERC20HelperContract { }); expectedDebt = 21_118.612213260575680078 * 1e18; + _assertPool( PoolParams({ htp: 424.349858731660857846 * 1e18, @@ -453,28 +421,26 @@ contract ERC20PoolBorrowTest is ERC20HelperContract { interestRateUpdate: _startTime + 30 days }) ); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: expectedDebt, - borrowerCollateral: 50 * 1e18, - borrowert0Np: 445.838278846153846359 * 1e18, - borrowerCollateralization: 7.057773002983275247 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: expectedDebt, + borrowerCollateral: 50 * 1e18, + borrowert0Np: 445.838278846153846359 * 1e18, + borrowerCollateralization: 7.057773002983275247 * 1e18 + }); _assertLenderInterest(liquidityAdded, 86.113113158840750000 * 1e18); skip(10 days); - _borrowZeroAmount( - { - from: _borrower, - amount: 0, - indexLimit: 3_000, - newLup: 2_981.007422784467321543 * 1e18 - } - ); + + _borrowZeroAmount({ + from: _borrower, + amount: 0, + indexLimit: 3_000, + newLup: 2_981.007422784467321543 * 1e18 + }); expectedDebt = 21_157.152643010853304038 * 1e18; + _assertPool( PoolParams({ htp: 425.900107294311861922 * 1e18, @@ -492,15 +458,13 @@ contract ERC20PoolBorrowTest is ERC20HelperContract { interestRateUpdate: _startTime + 40 days }) ); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: expectedDebt, - borrowerCollateral: 50 * 1e18, - borrowert0Np: 448.381722115384615591 * 1e18, - borrowerCollateralization: 7.044916376706357984 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: expectedDebt, + borrowerCollateral: 50 * 1e18, + borrowert0Np: 448.381722115384615591 * 1e18, + borrowerCollateralization: 7.044916376706357984 * 1e18 + }); _assertLenderInterest(liquidityAdded, 119.836959946754650000 * 1e18); skip(10 days); @@ -509,6 +473,7 @@ contract ERC20PoolBorrowTest is ERC20HelperContract { IERC20Pool(address(_pool)).drawDebt(_borrower, 0, 0, 0); expectedDebt = 21_199.628356897284442294 * 1e18; + _assertPool( PoolParams({ htp: 427.611922756860156608 * 1e18, @@ -526,19 +491,19 @@ contract ERC20PoolBorrowTest is ERC20HelperContract { interestRateUpdate: _startTime + 50 days }) ); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: expectedDebt, - borrowerCollateral: 50 * 1e18, - borrowert0Np: 448.381722115384615591 * 1e18, - borrowerCollateralization: 7.030801136225104190 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: expectedDebt, + borrowerCollateral: 50 * 1e18, + borrowert0Np: 448.381722115384615591 * 1e18, + borrowerCollateralization: 7.030801136225104190 * 1e18 + }); _assertLenderInterest(liquidityAdded, 157.005764521268350000 * 1e18); skip(10 days); + expectedDebt = 21_246.450141935843866714 * 1e18; + _assertPool( PoolParams({ htp: 427.611922756860156608 * 1e18, @@ -556,15 +521,13 @@ contract ERC20PoolBorrowTest is ERC20HelperContract { interestRateUpdate: _startTime + 50 days }) ); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: expectedDebt, - borrowerCollateral: 50 * 1e18, - borrowert0Np: 448.381722115384615591 * 1e18, - borrowerCollateralization: 7.015307034516347067 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: expectedDebt, + borrowerCollateral: 50 * 1e18, + borrowert0Np: 448.381722115384615591 * 1e18, + borrowerCollateralization: 7.015307034516347067 * 1e18 + }); } /** @@ -577,111 +540,90 @@ contract ERC20PoolBorrowTest is ERC20HelperContract { */ function testPoolBorrowReverts() external tearDown { // should revert if borrower attempts to borrow with an out of bounds limitIndex - _assertBorrowLimitIndexRevert( - { - from: _borrower, - amount: 1_000 * 1e18, - indexLimit: 1000 - } - ); + _assertBorrowLimitIndexRevert({ + from: _borrower, + amount: 1_000 * 1e18, + indexLimit: 1000 + }); // should revert if borrower tries to borrow on behalf of different address - _assertBorrowBorrowerNotSenderRevert( - { - from: _borrower, - borrower: _borrower2, - amount: 1 * 1e18, - indexLimit: 7000 - } - ); + _assertBorrowBorrowerNotSenderRevert({ + from: _borrower, + borrower: _borrower2, + amount: 1 * 1e18, + indexLimit: 7000 + }); // should revert if borrower didn't pledged any collateral - _assertBorrowBorrowerUnderCollateralizedRevert( - { - from: _borrower, - amount: 500 * 1e18, - indexLimit: 3000 - } - ); + _assertBorrowBorrowerUnderCollateralizedRevert({ + from: _borrower, + amount: 500 * 1e18, + indexLimit: 3000 + }); // borrower 1 borrows 500 quote from the pool after adding sufficient collateral - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - amount: 50 * 1e18 - } - ); - _borrow( - { - from: _borrower, - amount: 500 * 1e18, - indexLimit: 3_000, - newLup: 3_010.892022197881557845 * 1e18 - } - ); + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + amount: 50 * 1e18 + }); + _borrow({ + from: _borrower, + amount: 500 * 1e18, + indexLimit: 3_000, + newLup: 3_010.892022197881557845 * 1e18 + }); // borrower 2 borrows 15k quote from the pool with borrower2 becoming new queue HEAD - _pledgeCollateral( - { - from: _borrower2, - borrower: _borrower2, - amount: 6 * 1e18 - } - ); - _borrow( - { - from: _borrower2, - amount: 15_000 * 1e18, - indexLimit: 3_000, - newLup: 2_995.912459898389633881 * 1e18 - } - ); + _pledgeCollateral({ + from: _borrower2, + borrower: _borrower2, + amount: 6 * 1e18 + }); + _borrow({ + from: _borrower2, + amount: 15_000 * 1e18, + indexLimit: 3_000, + newLup: 2_995.912459898389633881 * 1e18 + }); // should revert if borrower undercollateralized - _assertBorrowBorrowerUnderCollateralizedRevert( - { - from: _borrower2, - amount: 2_976 * 1e18, - indexLimit: 3000 - } - ); + _assertBorrowBorrowerUnderCollateralizedRevert({ + from: _borrower2, + amount: 2_976 * 1e18, + indexLimit: 3000 + }); // should be able to borrow if properly specified - _borrow( - { - from: _borrower2, - amount: 10 * 1e18, - indexLimit: 3_000, - newLup: 2_995.912459898389633881 * 1e18 - } - ); + _borrow({ + from: _borrower2, + amount: 10 * 1e18, + indexLimit: 3_000, + newLup: 2_995.912459898389633881 * 1e18 + }); } function testMinBorrowAmountCheck() external tearDown { // 10 borrowers draw debt for (uint i=0; i<10; ++i) { - _anonBorrowerDrawsDebt(100 * 1e18, 1_200 * 1e18, 7777); + _anonBorrowerDrawsDebt(100 * 1e18, 1_200 * 1e18, MAX_FENWICK_INDEX); } + (, uint256 loansCount, , , ) = _poolUtils.poolLoansInfo(address(_pool)); assertEq(loansCount, 10); - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - amount: 100 * 1e18 - } - ); + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + amount: 100 * 1e18 + }); // should revert if borrower attempts to borrow more than minimum amount - _assertBorrowMinDebtRevert( - { - from: _borrower, - amount: 10 * 1e18, - indexLimit: 7_777 - } - ); + _assertBorrowMinDebtRevert({ + from: _borrower, + amount: 10 * 1e18, + indexLimit: MAX_FENWICK_INDEX + }); } /** @@ -695,38 +637,30 @@ contract ERC20PoolBorrowTest is ERC20HelperContract { deal(address(_quote), _borrower, _quote.balanceOf(_borrower) + 10_000 * 1e18); // should revert if borrower has no debt - _assertRepayNoDebtRevert( - { - from: _borrower, - borrower: _borrower, - amount: 10_000 * 1e18 - } - ); + _assertRepayNoDebtRevert({ + from: _borrower, + borrower: _borrower, + amount: 10_000 * 1e18 + }); - _assertPullBorrowerNotSenderRevert( - { - from: _borrower, - borrower: _borrower2, - amount: 10_000 * 1e18 - } - ); + _assertPullBorrowerNotSenderRevert({ + from: _borrower, + borrower: _borrower2, + amount: 10_000 * 1e18 + }); // borrower 1 borrows 1000 quote from the pool - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - amount: 50 * 1e18 - } - ); - _borrow( - { - from: _borrower, - amount: 1_000 * 1e18, - indexLimit: 3_000, - newLup: 3_010.892022197881557845 * 1e18 - } - ); + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + amount: 50 * 1e18 + }); + _borrow({ + from: _borrower, + amount: 1_000 * 1e18, + indexLimit: 3_000, + newLup: 3_010.892022197881557845 * 1e18 + }); _assertPool( PoolParams({ @@ -747,21 +681,17 @@ contract ERC20PoolBorrowTest is ERC20HelperContract { ); // borrower 2 borrows 5k quote from the pool and becomes new queue HEAD - _pledgeCollateral( - { - from: _borrower2, - borrower: _borrower2, - amount: 50 * 1e18 - } - ); - _borrow( - { - from: _borrower2, - amount: 5_000 * 1e18, - indexLimit: 3_000, - newLup: 3_010.892022197881557845 * 1e18 - } - ); + _pledgeCollateral({ + from: _borrower2, + borrower: _borrower2, + amount: 50 * 1e18 + }); + _borrow({ + from: _borrower2, + amount: 5_000 * 1e18, + indexLimit: 3_000, + newLup: 3_010.892022197881557845 * 1e18 + }); _assertPool( PoolParams({ @@ -781,6 +711,14 @@ contract ERC20PoolBorrowTest is ERC20HelperContract { }) ); + // should revert if LUP is below the limit + ( , , , , , uint256 lupIndex ) = _poolUtils.poolPricesInfo(address(_pool)); + _assertPullLimitIndexRevert({ + from: _borrower, + amount: 20 * 1e18, + indexLimit: lupIndex - 1 + }); + // should be able to repay loan if properly specified _repayDebt({ from: _borrower, @@ -813,48 +751,43 @@ contract ERC20PoolBorrowTest is ERC20HelperContract { function testMinRepayAmountCheck() external tearDown { // borrower 1 borrows 1000 quote from the pool _drawDebt({ - from: _borrower, - borrower: _borrower, - amountToBorrow: 1_000 * 1e18, - limitIndex: 3_000, + from: _borrower, + borrower: _borrower, + amountToBorrow: 1_000 * 1e18, + limitIndex: 3_000, collateralToPledge: 50 * 1e18, - newLup: 3_010.892022197881557845 * 1e18 + newLup: 3_010.892022197881557845 * 1e18 }); // 9 other borrowers draw debt for (uint i=0; i<9; ++i) { - _anonBorrowerDrawsDebt(100 * 1e18, 1_000 * 1e18, 7777); + _anonBorrowerDrawsDebt(100 * 1e18, 1_000 * 1e18, MAX_FENWICK_INDEX); } + (, uint256 loansCount, , , ) = _poolUtils.poolLoansInfo(address(_pool)); assertEq(loansCount, 10); // should revert if amount left after repay is less than the average debt - _assertRepayMinDebtRevert( - { - from: _borrower, - borrower: _borrower, - amount: 950 * 1e18 - } - ); + _assertRepayMinDebtRevert({ + from: _borrower, + borrower: _borrower, + amount: 950 * 1e18 + }); } function testRepayLoanFromDifferentActor() external tearDown { // borrower 1 borrows 1000 quote from the pool - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - amount: 50 * 1e18 - } - ); - _borrow( - { - from: _borrower, - amount: 1_000 * 1e18, - indexLimit: 3_000, - newLup: 3_010.892022197881557845 * 1e18 - } - ); + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + amount: 50 * 1e18 + }); + _borrow({ + from: _borrower, + amount: 1_000 * 1e18, + indexLimit: 3_000, + newLup: 3_010.892022197881557845 * 1e18 + }); _assertPool( PoolParams({ @@ -910,25 +843,23 @@ contract ERC20PoolBorrowTest is ERC20HelperContract { */ function testZeroThresholdPriceLoan() external tearDown { // borrower 1 initiates a highly overcollateralized loan with a TP of 0 that won't be inserted into the Queue - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - amount: 50 * 1e18 - } - ); + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + amount: 50 * 1e18 + }); + vm.expectRevert(abi.encodeWithSignature('ZeroThresholdPrice()')); IERC20Pool(address(_pool)).drawDebt(_borrower, 0.00000000000000001 * 1e18, 3000, 0); - // borrower 1 borrows 500 quote from the pool after using a non 0 TP _drawDebt({ - from: _borrower, - borrower: _borrower, - amountToBorrow: 500 * 1e18, - limitIndex: 3_000, + from: _borrower, + borrower: _borrower, + amountToBorrow: 500 * 1e18, + limitIndex: 3_000, collateralToPledge: 50 * 1e18, - newLup: 3_010.892022197881557845 * 1e18 + newLup: 3_010.892022197881557845 * 1e18 }); _assertPool( @@ -985,19 +916,19 @@ contract ERC20PoolBorrowTest is ERC20HelperContract { }) ); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 500.480769230769231 * 1e18, - borrowerCollateral: 50 * 1e18, - borrowert0Np: 10.510096153846153851 * 1e18, - borrowerCollateralization: 300.799971477982403259 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 500.480769230769231 * 1e18, + borrowerCollateral: 50 * 1e18, + borrowert0Np: 10.510096153846153851 * 1e18, + borrowerCollateralization: 300.799971477982403259 * 1e18 + }); + deal(address(_quote), _borrower, _quote.balanceOf(_borrower) + 10_000 * 1e18); + // should revert if borrower repays most, but not all of their debt resulting in a 0 tp loan remaining on the book vm.expectRevert(abi.encodeWithSignature('ZeroThresholdPrice()')); - IERC20Pool(address(_pool)).repayDebt(_borrower, 500.480769230769231000 * 1e18 - 1, 0); + IERC20Pool(address(_pool)).repayDebt(_borrower, 500.480769230769231000 * 1e18 - 1, 0, _borrower, MAX_FENWICK_INDEX); // should be able to pay back all pendingDebt _repayDebt({ @@ -1032,70 +963,66 @@ contract ERC20PoolBorrowTest is ERC20HelperContract { // check balances before borrow assertEq(_quote.balanceOf(_lender), 150_000 * 1e18); - _assertLenderLpBalance( - { - lender: _lender, - index: highest, - lpBalance: 10_000 * 1e27, - depositTime: _startTime - } - ); + _assertLenderLpBalance({ + lender: _lender, + index: highest, + lpBalance: 10_000 * 1e18, + depositTime: _startTime + }); + assertEq(_quote.balanceOf(_borrower), 0); assertEq(_collateral.balanceOf(_borrower), 100 * 1e18); // pledge and borrow - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - amount: 100 * 1e18 - } - ); - _borrow( - { - from: _borrower, - amount: 21_000 * 1e18, - indexLimit: 3_000, - newLup: 2_981.007422784467321543 * 1e18 - } - ); + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + amount: 100 * 1e18 + }); + _borrow({ + from: _borrower, + amount: 21_000 * 1e18, + indexLimit: 3_000, + newLup: 2_981.007422784467321543 * 1e18 + }); + assertEq(_quote.balanceOf(_borrower), 21_000 * 1e18); assertEq(_collateral.balanceOf(_borrower), 0); - _assertPoolPrices( - { - htp: 210.201923076923077020 * 1e18, - htpIndex: 3_083, - hpb: 3_010.892022197881557845 * 1e18, - hpbIndex: 2550, - lup: 2_981.007422784467321543 * 1e18, - lupIndex: 2_552 - } - ); + _assertPoolPrices({ + htp: 210.201923076923077020 * 1e18, + htpIndex: 3_083, + hpb: 3_010.892022197881557845 * 1e18, + hpbIndex: 2550, + lup: 2_981.007422784467321543 * 1e18, + lupIndex: 2_552 + }); + + _addLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: _indexOf(200 * 1e18), + lpAward: 10_000 * 1e18, + newLup: 2_981.007422784467321543 * 1e18 + }); + // penalty should not be applied on buckets with prices lower than PTP - _addLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: _indexOf(200 * 1e18), - lpAward: 10_000 * 1e27, - newLup: 2_981.007422784467321543 * 1e18 - } - ); + assertEq(_quote.balanceOf(_lender), 140_000 * 1e18); - _removeAllLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: _indexOf(200 * 1e18), - newLup: 2_981.007422784467321543 * 1e18, - lpRedeem: 10_000 * 1e27 - } - ); + + _removeAllLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: _indexOf(200 * 1e18), + newLup: 2_981.007422784467321543 * 1e18, + lpRedeem: 10_000 * 1e18 + }); + assertEq(_quote.balanceOf(_lender), 150_000 * 1e18); // no tokens paid as penalty // repay entire loan deal(address(_quote), _borrower, _quote.balanceOf(_borrower) + 40 * 1e18); + _repayDebt({ from: _borrower, borrower: _borrower, @@ -1104,31 +1031,32 @@ contract ERC20PoolBorrowTest is ERC20HelperContract { collateralToPull: 0, newLup: MAX_PRICE }); + assertEq(_quote.balanceOf(_borrower), 19.807692307692298000 * 1e18); assertEq(_collateral.balanceOf(_borrower), 0); - _assertPoolPrices( - { - htp: 0, - htpIndex: 7_388, - hpb: 3_010.892022197881557845 * 1e18, - hpbIndex: 2550, - lup: MAX_PRICE, - lupIndex: 0 - } - ); + _assertPoolPrices({ + htp: 0, + htpIndex: 7_388, + hpb: 3_010.892022197881557845 * 1e18, + hpbIndex: 2550, + lup: MAX_PRICE, + lupIndex: 0 + }); + // lender removes everything from above PTP, penalty should be applied uint256 snapshot = vm.snapshot(); - _removeAllLiquidity( - { - from: _lender, - amount: 9_990.384615384615380000 * 1e18, - index: highest, - newLup: MAX_PRICE, - lpRedeem: 10_000 * 1e27 - } - ); + + _removeAllLiquidity({ + from: _lender, + amount: 9_990.384615384615380000 * 1e18, + index: highest, + newLup: MAX_PRICE, + lpRedeem: 10_000 * 1e18 + }); + assertEq(_quote.balanceOf(_lender), 159_990.384615384615380000 * 1e18); // 5 tokens paid as penalty + vm.revertTo(snapshot); // borrower pulls first all their collateral pledged, PTP goes to 0, penalty should be applied @@ -1139,30 +1067,31 @@ contract ERC20PoolBorrowTest is ERC20HelperContract { amountRepaid: 0, collateralToPull: 100 * 1e18 }); + assertEq(_quote.balanceOf(_borrower), 19.807692307692298000 * 1e18); assertEq(_collateral.balanceOf(_borrower), 100 * 1e18); - _removeAllLiquidity( - { - from: _lender, - amount: 9_990.384615384615380000 * 1e18, - index: highest, - newLup: MAX_PRICE, - lpRedeem: 10_000 * 1e27 - } - ); + + _removeAllLiquidity({ + from: _lender, + amount: 9_990.384615384615380000 * 1e18, + index: highest, + newLup: MAX_PRICE, + lpRedeem: 10_000 * 1e18 + }); + assertEq(_quote.balanceOf(_lender), 159_990.384615384615380000 * 1e18); // 5 tokens paid as penalty // lender removes everything from price above PTP after 24 hours, penalty should not be applied skip(1 days); - _removeAllLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: med, - newLup: MAX_PRICE, - lpRedeem: 10_000 * 1e27 - } - ); + + _removeAllLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: med, + newLup: MAX_PRICE, + lpRedeem: 10_000 * 1e18 + }); + assertEq(_quote.balanceOf(_lender), 169_990.384615384615380000 * 1e18); // no tokens paid as penalty } } @@ -1192,52 +1121,42 @@ contract ERC20PoolBorrowFuzzyTest is ERC20FuzzyHelperContract { _mintQuoteAndApproveTokens(_lender, 200_000 * 1e18); _mintQuoteAndApproveTokens(_lender1, 200_000 * 1e18); - // lender deposits 10000 DAI in 5 buckets each - _addLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: highest, - lpAward: 10_000 * 1e27, - newLup: MAX_PRICE - } - ); - _addLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: high, - lpAward: 10_000 * 1e27, - newLup: MAX_PRICE - } - ); - _addLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: med, - lpAward: 10_000 * 1e27, - newLup: MAX_PRICE - } - ); - _addLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: low, - lpAward: 10_000 * 1e27, - newLup: MAX_PRICE - } - ); - _addLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: lowest, - lpAward: 10_000 * 1e27, - newLup: MAX_PRICE - } - ); + // lender deposits 10000 quote tokens in 5 buckets each + _addLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: highest, + lpAward: 10_000 * 1e18, + newLup: MAX_PRICE + }); + _addLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: high, + lpAward: 10_000 * 1e18, + newLup: MAX_PRICE + }); + _addLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: med, + lpAward: 10_000 * 1e18, + newLup: MAX_PRICE + }); + _addLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: low, + lpAward: 10_000 * 1e18, + newLup: MAX_PRICE + }); + _addLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: lowest, + lpAward: 10_000 * 1e18, + newLup: MAX_PRICE + }); _assertPool( PoolParams({ @@ -1267,22 +1186,23 @@ contract ERC20PoolBorrowFuzzyTest is ERC20FuzzyHelperContract { uint256[] memory indexes = new uint256[](numIndexes); for (uint256 i = 0; i < numIndexes; ++i) { deal(address(_quote), _lender, mintAmount_); + indexes[i] = _randomIndex(); _addLiquidity({ from: _lender, amount: mintAmount_, index: indexes[i], - lpAward: mintAmount_ * 1e9, + lpAward: mintAmount_, newLup: _calculateLup(address(_pool), 0) }); _assertBucket({ index: indexes[i], - lpBalance: mintAmount_ * 1e9, + lpBalance: mintAmount_, collateral: 0, deposit: mintAmount_, - exchangeRate: 1e27 + exchangeRate: 1e18 }); } @@ -1291,7 +1211,9 @@ contract ERC20PoolBorrowFuzzyTest is ERC20FuzzyHelperContract { uint256 limitIndex = _findLowestIndexPrice(indexes); uint256 borrowAmount = Maths.wdiv(mintAmount_, Maths.wad(3)); uint256 requiredCollateral = _requiredCollateral(Maths.wdiv(mintAmount_, Maths.wad(3)), limitIndex); + deal(address(_collateral), _borrower, requiredCollateral); + _drawDebt({ from: _borrower, borrower: _borrower, @@ -1305,10 +1227,10 @@ contract ERC20PoolBorrowFuzzyTest is ERC20FuzzyHelperContract { for (uint256 i = 0; i < numIndexes; ++i) { _assertBucket({ index: indexes[i], - lpBalance: mintAmount_ * 1e9, + lpBalance: mintAmount_, collateral: 0, deposit: mintAmount_, - exchangeRate: 1e27 + exchangeRate: 1e18 }); } @@ -1318,6 +1240,7 @@ contract ERC20PoolBorrowFuzzyTest is ERC20FuzzyHelperContract { // check pool state (uint256 minDebt, , uint256 poolActualUtilization, uint256 poolTargetUtilization) = _poolUtils.poolUtilizationInfo(address(_pool)); + _assertPool( PoolParams({ htp: Maths.wdiv(debt, requiredCollateral), @@ -1335,6 +1258,7 @@ contract ERC20PoolBorrowFuzzyTest is ERC20FuzzyHelperContract { interestRateUpdate: _startTime }) ); + assertLt(_htp(), _poolUtils.lup(address(_pool))); assertGt(minDebt, 0); assertEq(_poolUtils.lup(address(_pool)), _calculateLup(address(_pool), debt)); @@ -1344,7 +1268,9 @@ contract ERC20PoolBorrowFuzzyTest is ERC20FuzzyHelperContract { // repay all debt and withdraw collateral (debt, , ) = _poolUtils.borrowerInfo(address(_pool), address(_borrower)); + deal(address(_quote), _borrower, debt); + _repayDebt({ from: _borrower, borrower: _borrower, @@ -1361,16 +1287,17 @@ contract ERC20PoolBorrowFuzzyTest is ERC20FuzzyHelperContract { // check that only deposits above the htp earned interest if (indexes[i] <= _poolUtils.priceToIndex(Maths.wdiv(debt, requiredCollateral))) { assertGt(deposit, mintAmount_); - assertGt(exchangeRate, 1e27); + assertGt(exchangeRate, 1e18); } else { assertEq(deposit, mintAmount_); - assertEq(exchangeRate, 1e27); + assertEq(exchangeRate, 1e18); } - assertEq(lpAccumulator, mintAmount_ * 1e9); + assertEq(lpAccumulator, mintAmount_); + _assertBucket({ index: indexes[i], - lpBalance: mintAmount_ * 1e9, + lpBalance: mintAmount_, collateral: 0, deposit: deposit, exchangeRate: exchangeRate diff --git a/tests/forge/ERC20Pool/ERC20PoolCollateral.t.sol b/tests/forge/ERC20Pool/ERC20PoolCollateral.t.sol index 6d698e413..f88d638ad 100644 --- a/tests/forge/ERC20Pool/ERC20PoolCollateral.t.sol +++ b/tests/forge/ERC20Pool/ERC20PoolCollateral.t.sol @@ -29,30 +29,24 @@ contract ERC20PoolCollateralTest is ERC20HelperContract { /** * @notice With 1 lender and 1 borrower test pledgeCollateral, borrow, and pullCollateral. */ - function testAddPullCollateral() external tearDown { + function testPledgeAndPullCollateral() external tearDown { // lender deposits 10000 Quote into 3 buckets - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 2550 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 2551 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 2552 - } - ); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2550 + }); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2551 + }); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2552 + }); _assertPool( PoolParams({ @@ -71,24 +65,21 @@ contract ERC20PoolCollateralTest is ERC20HelperContract { interestRateUpdate: _startTime }) ); + assertEq(_collateral.balanceOf(_borrower), 150 * 1e18); // borrower pledge 100 collateral and get a 21_000 Quote loan - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - amount: 100 * 1e18 - } - ); - _borrow( - { - from: _borrower, - amount: 21_000 * 1e18, - indexLimit: 3_000, - newLup: 2_981.007422784467321543 * 1e18 - } - ); + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + amount: 100 * 1e18 + }); + _borrow({ + from: _borrower, + amount: 21_000 * 1e18, + indexLimit: 3_000, + newLup: 2_981.007422784467321543 * 1e18 + }); _assertPool( PoolParams({ @@ -107,15 +98,14 @@ contract ERC20PoolCollateralTest is ERC20HelperContract { interestRateUpdate: _startTime }) ); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 21_020.192307692307702000 * 1e18, - borrowerCollateral: 100 * 1e18, - borrowert0Np: 220.712019230769230871 * 1e18, - borrowerCollateralization: 14.181637252165253251 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 21_020.192307692307702000 * 1e18, + borrowerCollateral: 100 * 1e18, + borrowert0Np: 220.712019230769230871 * 1e18, + borrowerCollateralization: 14.181637252165253251 * 1e18 + }); + assertEq(_collateral.balanceOf(_borrower), 50 * 1e18); // pass time to allow interest to accrue @@ -147,15 +137,14 @@ contract ERC20PoolCollateralTest is ERC20HelperContract { interestRateUpdate: _startTime + 10 days }) ); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 21_049.006823139002918431 * 1e18, - borrowerCollateral: 50 * 1e18, - borrowert0Np: 441.424038461538461742 * 1e18, - borrowerCollateralization: 7.081111825921092812 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 21_049.006823139002918431 * 1e18, + borrowerCollateral: 50 * 1e18, + borrowert0Np: 441.424038461538461742 * 1e18, + borrowerCollateralization: 7.081111825921092812 * 1e18 + }); + assertEq(_collateral.balanceOf(_borrower), 100 * 1e18); // remove all of the remaining claimable collateral @@ -184,39 +173,150 @@ contract ERC20PoolCollateralTest is ERC20HelperContract { interestRateUpdate: _startTime + 10 days }) ); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 21_049.006823139002918431 * 1e18, - borrowerCollateral: 7.061038044473493202 * 1e18, - borrowert0Np: 3_140.657612229160876676 * 1e18, - borrowerCollateralization: 1 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 21_049.006823139002918431 * 1e18, + borrowerCollateral: 7.061038044473493202 * 1e18, + borrowert0Np: 3_140.657612229160876676 * 1e18, + borrowerCollateralization: 1 * 1e18 + }); + assertEq(_collateral.balanceOf(_borrower), 142.938961955526506798 * 1e18); } + /** + * @notice With 1 lender and 1 borrower test pledgeCollateral, borrow, pull and transfer collateral to a different recipient. + */ + function testPledgeAndPullCollateralToDifferentRecipient() external tearDown { + // lender deposits 10000 Quote into 3 buckets + + address collateralReceiver = makeAddr("receiver"); + + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2550 + }); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2551 + }); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2552 + }); + + assertEq(_collateral.balanceOf(collateralReceiver), 0); + assertEq(_collateral.balanceOf(_borrower), 150 * 1e18); + + // borrower pledge 100 collateral and get a 21_000 Quote loan + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + amount: 100 * 1e18 + }); + _borrow({ + from: _borrower, + amount: 21_000 * 1e18, + indexLimit: 3_000, + newLup: 2_981.007422784467321543 * 1e18 + }); + + _assertPool( + PoolParams({ + htp: 210.201923076923077020 * 1e18, + lup: 2_981.007422784467321543 * 1e18, + poolSize: 30_000 * 1e18, + pledgedCollateral: 100 * 1e18, + encumberedCollateral: 7.051372011699988577 * 1e18, + poolDebt: 21_020.192307692307702000 * 1e18, + actualUtilization: 0.700673076923076923 * 1e18, + targetUtilization: 1e18, + minDebtAmount: 2_102.019230769230770200 * 1e18, + loans: 1, + maxBorrower: _borrower, + interestRate: 0.05 * 1e18, + interestRateUpdate: _startTime + }) + ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 21_020.192307692307702000 * 1e18, + borrowerCollateral: 100 * 1e18, + borrowert0Np: 220.712019230769230871 * 1e18, + borrowerCollateralization: 14.181637252165253251 * 1e18 + }); + + assertEq(_collateral.balanceOf(collateralReceiver), 0); + assertEq(_collateral.balanceOf(_borrower), 50 * 1e18); + + // pass time to allow interest to accrue + skip(10 days); + + // remove some of the collateral and transfer to recipient + _repayDebtAndPullToRecipient({ + from: _borrower, + borrower: _borrower, + recipient: collateralReceiver, + amountToRepay: 0, + amountRepaid: 0, + collateralToPull: 50 * 1e18, + newLup: 2_981.007422784467321543 * 1e18 + }); + + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 21_049.006823139002918431 * 1e18, + borrowerCollateral: 50 * 1e18, + borrowert0Np: 441.424038461538461742 * 1e18, + borrowerCollateralization: 7.081111825921092812 * 1e18 + }); + + assertEq(_collateral.balanceOf(collateralReceiver), 50 * 1e18); + assertEq(_collateral.balanceOf(_borrower), 50 * 1e18); + + // remove all of the remaining claimable collateral + _repayDebtAndPullToRecipient({ + from: _borrower, + borrower: _borrower, + recipient: collateralReceiver, + amountToRepay: 0, + amountRepaid: 0, + collateralToPull: 50 * 1e18 - _encumberance(21_049.006823139002918431 * 1e18, _lup()), + newLup: 2_981.007422784467321543 * 1e18 + }); + + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 21_049.006823139002918431 * 1e18, + borrowerCollateral: 7.061038044473493202 * 1e18, + borrowert0Np: 3_140.657612229160876676 * 1e18, + borrowerCollateralization: 1 * 1e18 + }); + + assertEq(_collateral.balanceOf(collateralReceiver), 92.938961955526506798 * 1e18); + assertEq(_collateral.balanceOf(_borrower), 50 * 1e18); + } + /** * @notice 1 borrower tests reverts in pullCollateral. * Reverts: * Attempts to remove more than available claimable collateral. */ function testPullCollateralRequireEnoughCollateral() external tearDown { - _assertPullInsufficientCollateralRevert( - { - from: _borrower, - amount: 100 * 1e18 - } - ); + _assertPullInsufficientCollateralRevert({ + from: _borrower, + amount: 100 * 1e18 + }); // borrower deposits 100 collateral - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - amount: 100 * 1e18 - } - ); + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + amount: 100 * 1e18 + }); // should be able to now remove collateral _repayDebtNoLupCheck({ @@ -236,105 +336,90 @@ contract ERC20PoolCollateralTest is ERC20HelperContract { _mintCollateralAndApproveTokens(_bidder, 100 * 1e18); // should revert if adding collateral at index 0 - _assertAddCollateralAtIndex0Revert( - { - from: _bidder, - amount: 4 * 1e18 - } - ); + _assertAddCollateralAtIndex0Revert({ + from: _bidder, + amount: 4 * 1e18 + }); // actor deposits collateral into a bucket - _addCollateral( - { - from: _bidder, - amount: 4 * 1e18, - index: 2550, - lpAward: 12_043.56808879152623138 * 1e27 - } - ); + _addCollateral({ + from: _bidder, + amount: 4 * 1e18, + index: 2550, + lpAward: 12_043.56808879152623138 * 1e18 + }); // check bucket state and bidder's LPs - _assertBucket( - { - index: 2550, - lpBalance: 12_043.56808879152623138 * 1e27, - collateral: 4 * 1e18, - deposit: 0, - exchangeRate: 1 * 1e27 - } - ); - _assertLenderLpBalance( - { - lender: _bidder, - index: 2550, - lpBalance: 12_043.56808879152623138 * 1e27, - depositTime: _startTime - } - ); + _assertBucket({ + index: 2550, + lpBalance: 12_043.56808879152623138 * 1e18, + collateral: 4 * 1e18, + deposit: 0, + exchangeRate: 1 * 1e18 + }); + _assertLenderLpBalance({ + lender: _bidder, + index: 2550, + lpBalance: 12_043.56808879152623138 * 1e18, + depositTime: _startTime + }); + // check balances assertEq(_collateral.balanceOf(_bidder), 96 * 1e18); assertEq(_collateral.balanceOf(address(_pool)), 4 * 1e18); assertEq(_quote.balanceOf(address(_pool)), 0); // actor withdraws some of their collateral - _removeCollateral( - { - from: _bidder, - amount: 1.53 * 1e18, - index: 2550, - lpRedeem: 4_606.664793962758783502850000000 * 1e27 - } - ); + _removeCollateral({ + from: _bidder, + amount: 1.53 * 1e18, + index: 2550, + lpRedeem: 4_606.664793962758783503 * 1e18 + }); + // check bucket state and bidder's LPs - _assertBucket( - { - index: 2550, - lpBalance: 7_436.90329482876744787715 * 1e27, - collateral: 2.47 * 1e18, - deposit: 0, - exchangeRate: 1 * 1e27 - } - ); - _assertLenderLpBalance( - { - lender: _bidder, - index: 2550, - lpBalance: 7_436.90329482876744787715 * 1e27, - depositTime: _startTime - } - ); + _assertBucket({ + index: 2550, + lpBalance: 7_436.903294828767447877 * 1e18, + collateral: 2.47 * 1e18, + deposit: 0, + exchangeRate: 1 * 1e18 + }); + _assertLenderLpBalance({ + lender: _bidder, + index: 2550, + lpBalance: 7_436.903294828767447877 * 1e18, + depositTime: _startTime + }); + // check balances assertEq(_collateral.balanceOf(_bidder), 97.53 * 1e18); assertEq(_collateral.balanceOf(address(_pool)), 2.47 * 1e18); assertEq(_quote.balanceOf(address(_pool)), 0); // actor withdraws remainder of their _collateral - _removeCollateral( - { - from: _bidder, - amount: 2.47 * 1e18, - index: 2550, - lpRedeem: 7_436.90329482876744787715 * 1e27 - } - ); + _removeCollateral({ + from: _bidder, + amount: 2.47 * 1e18, + index: 2550, + lpRedeem: 7_436.903294828767447877 * 1e18 + }); + // check bucket state and bidder's LPs - _assertBucket( - { - index: 2550, - lpBalance: 0, - collateral: 0, - deposit: 0, - exchangeRate: 1 * 1e27 - } - ); - _assertLenderLpBalance( - { - lender: _bidder, - index: 2550, - lpBalance: 0, - depositTime: _startTime - } - ); + _assertBucket({ + index: 2550, + lpBalance: 0, + collateral: 0, + deposit: 0, + exchangeRate: 1 * 1e18 + }); + _assertLenderLpBalance({ + lender: _bidder, + index: 2550, + lpBalance: 0, + depositTime: _startTime + }); + // check balances assertEq(_collateral.balanceOf(_bidder), 100 * 1e18); assertEq(_collateral.balanceOf(address(_pool)), 0); @@ -346,73 +431,63 @@ contract ERC20PoolCollateralTest is ERC20HelperContract { _mintCollateralAndApproveTokens(_bidder, 1 * 1e18); // actor deposits collateral into a bucket - _addCollateral( - { - from: _bidder, - amount: 1 * 1e18, - index: 1530, - lpAward: 487616.252661175041981841 * 1e27 - } - ); + _addCollateral({ + from: _bidder, + amount: 1 * 1e18, + index: 1530, + lpAward: 487616.252661175041981841 * 1e18 + }); + + _removeCollateral({ + from: _bidder, + amount: 0.5 * 1e18, + index: 1530, + lpRedeem: 243_808.126330587520990921 * 1e18 + }); - _removeCollateral( - { - from: _bidder, - amount: 0.5 * 1e18, - index: 1530, - lpRedeem: 243_808.1263305875209909205 * 1e27 - } - ); // check bucket state and bidder's LPs - _assertBucket( - { - index: 1530, - lpBalance: 243_808.1263305875209909205 * 1e27, - collateral: 0.5 * 1e18, - deposit: 0, - exchangeRate: 1 * 1e27 - } - ); - _assertLenderLpBalance( - { - lender: _bidder, - index: 1530, - lpBalance: 243_808.1263305875209909205 * 1e27, - depositTime: _startTime - } - ); + _assertBucket({ + index: 1530, + lpBalance: 243_808.126330587520990920 * 1e18, + collateral: 0.5 * 1e18, + deposit: 0, + exchangeRate: 1 * 1e18 + }); + _assertLenderLpBalance({ + lender: _bidder, + index: 1530, + lpBalance: 243_808.126330587520990920 * 1e18, + depositTime: _startTime + }); + // check balances assertEq(_collateral.balanceOf(_bidder), 0.5 * 1e18); assertEq(_collateral.balanceOf(address(_pool)), 0.5 * 1e18); assertEq(_quote.balanceOf(address(_pool)), 0); // actor withdraws remainder of their _collateral - _removeAllCollateral( - { - from: _bidder, - amount: 0.5 * 1e18, - index: 1530, - lpRedeem: 243_808.1263305875209909205 * 1e27 - } - ); + _removeAllCollateral({ + from: _bidder, + amount: 0.5 * 1e18, + index: 1530, + lpRedeem: 243_808.126330587520990920 * 1e18 + }); + // check bucket state and bidder's LPs - _assertBucket( - { - index: 1530, - lpBalance: 0, - collateral: 0, - deposit: 0, - exchangeRate: 1 * 1e27 - } - ); - _assertLenderLpBalance( - { - lender: _bidder, - index: 1530, - lpBalance: 0, - depositTime: _startTime - } - ); + _assertBucket({ + index: 1530, + lpBalance: 0, + collateral: 0, + deposit: 0, + exchangeRate: 1 * 1e18 + }); + _assertLenderLpBalance({ + lender: _bidder, + index: 1530, + lpBalance: 0, + depositTime: _startTime + }); + // check balances assertEq(_collateral.balanceOf(_bidder), 1 * 1e18); assertEq(_collateral.balanceOf(address(_pool)), 0 * 1e18); @@ -423,42 +498,44 @@ contract ERC20PoolCollateralTest is ERC20HelperContract { uint256 testIndex = 6348; // should revert if no collateral in the bucket - _assertRemoveInsufficientCollateralRevert( - { - from: _lender, - amount: 3.50 * 1e18, - index: testIndex - } - ); + _assertRemoveInsufficientCollateralRevert({ + from: _lender, + amount: 3.50 * 1e18, + index: testIndex + }); // another actor deposits some collateral deal(address(_collateral), _bidder, 100 * 1e18); + changePrank(_bidder); _collateral.approve(address(_pool), 100 * 1e18); - _addCollateral( - { - from: _bidder, - amount: 0.65 * 1e18, - index: testIndex, - lpAward: 0.0000116119721720119 * 1e27 - } - ); + + _addCollateral({ + from: _bidder, + amount: 0.65 * 1e18, + index: testIndex, + lpAward: 0.000011611972172012 * 1e18 + }); // should revert if actor has no LPB in the bucket - _assertRemoveAllCollateralNoClaimRevert( - { - from: _lender, - index: testIndex - } - ); + _assertRemoveAllCollateralNoClaimRevert({ + from: _lender, + index: testIndex + }); // should revert if actor does not have LP - _assertRemoveAllCollateralNoClaimRevert( - { - from: _lender, - index: testIndex - } - ); + _assertRemoveAllCollateralNoClaimRevert({ + from: _lender, + index: testIndex + }); + + // should revert if expiration passed + _assertAddCollateralExpiredRevert({ + from: _lender, + amount: 0.5 * 1e18, + index: testIndex, + expiry: block.timestamp - 2 minutes + }); } function testPledgeCollateralFromDifferentActor() external tearDown { @@ -480,17 +557,16 @@ contract ERC20PoolCollateralTest is ERC20HelperContract { interestRateUpdate: _startTime }) ); + assertEq(_collateral.balanceOf(_borrower), 150 * 1e18); assertEq(_collateral.balanceOf(_borrower2), 100 * 1e18); // borrower deposits 100 collateral - _pledgeCollateral( - { - from: _borrower2, - borrower: _borrower2, - amount: 100 * 1e18 - } - ); + _pledgeCollateral({ + from: _borrower2, + borrower: _borrower2, + amount: 100 * 1e18 + }); // check pool state collateral accounting updated properly _assertPool( @@ -510,7 +586,158 @@ contract ERC20PoolCollateralTest is ERC20HelperContract { interestRateUpdate: _startTime }) ); + assertEq(_collateral.balanceOf(_borrower), 150 * 1e18); assertEq(_collateral.balanceOf(_borrower2), 0); } + + function testAddRemoveCollateralBucketExchangeRateInvariantDifferentActor() external tearDown { + _mintCollateralAndApproveTokens(_lender, 50000000000 * 1e18); + + _addInitialLiquidity({ + from: _bidder, + amount: 6879, + index: 2570 + }); + + _assertLenderLpBalance({ + lender: _lender, + index: 2570, + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: _bidder, + index: 2570, + lpBalance: 6879, + depositTime: _startTime + }); + _assertBucket({ + index: 2570, + lpBalance: 6879, + collateral: 0, + deposit: 6879, + exchangeRate: 1 * 1e18 // exchange rate should not change + }); + + _addCollateral({ + from: _lender, + amount: 3642907759.282013932739218713 * 1e18, + index: 2570, + lpAward: 9927093687851.086595628225711617 * 1e18 + }); + + _assertLenderLpBalance({ + lender: _lender, + index: 2570, + lpBalance: 9927093687851.086595628225711617 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: _bidder, + index: 2570, + lpBalance: 6879, + depositTime: _startTime + }); + _assertBucket({ + index: 2570, + lpBalance: 9927093687851.086595628225718496 * 1e18, + collateral: 3642907759.282013932739218713 * 1e18, + deposit: 6879, + exchangeRate: 1 * 1e18 // exchange rate should not change + }); + + _removeAllCollateral({ + from: _lender, + amount: 3642907759.282013932739218713 * 1e18, + index: 2570, + lpRedeem: 9927093687851.086595628225711617 * 1e18 + }); + + _assertLenderLpBalance({ + lender: _lender, + index: 2570, + lpBalance: 0, // LPs should get back to same value as before add / remove collateral + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: _bidder, + index: 2570, + lpBalance: 6879, // LPs should get back to same value as before add / remove collateral + depositTime: _startTime + }); + _assertBucket({ + index: 2570, + lpBalance: 6879, + collateral: 0, + deposit: 6879, + exchangeRate: 1 * 1e18 // exchange rate should not change + }); + } + + function testAddRemoveCollateralBucketExchangeRateInvariantSameActor() external tearDown { + _mintCollateralAndApproveTokens(_lender, 50000000000 * 1e18); + + _addInitialLiquidity({ + from: _lender, + amount: 6879, + index: 2570 + }); + + _assertLenderLpBalance({ + lender: _lender, + index: 2570, + lpBalance: 6879, + depositTime: _startTime + }); + _assertBucket({ + index: 2570, + lpBalance: 6879, + collateral: 0, + deposit: 6879, + exchangeRate: 1 * 1e18 // exchange rate should not change + }); + + _addCollateral({ + from: _lender, + amount: 3642907759.282013932739218713 * 1e18, + index: 2570, + lpAward: 9927093687851.086595628225711617 * 1e18 + }); + + _assertLenderLpBalance({ + lender: _lender, + index: 2570, + lpBalance: 9927093687851.086595628225718496 * 1e18, + depositTime: _startTime + }); + _assertBucket({ + index: 2570, + lpBalance: 9927093687851.086595628225718496 * 1e18, + collateral: 3642907759.282013932739218713 * 1e18, + deposit: 6879, + exchangeRate: 1 * 1e18 // exchange rate should not change + }); + + _removeAllCollateral({ + from: _lender, + amount: 3642907759.282013932739218713 * 1e18, + index: 2570, + lpRedeem: 9927093687851.086595628225711617 * 1e18 + }); + + _assertLenderLpBalance({ + lender: _lender, + index: 2570, + lpBalance: 6879, // LPs should get back to same value as before add / remove collateral + depositTime: _startTime + }); + _assertBucket({ + index: 2570, + lpBalance: 6879, + collateral: 0, + deposit: 6879, + exchangeRate: 1 * 1e18 // exchange rate should not change + }); + } } diff --git a/tests/forge/ERC20Pool/ERC20PoolFactory.t.sol b/tests/forge/ERC20Pool/ERC20PoolFactory.t.sol index c7b1d9015..a9e8d58f9 100644 --- a/tests/forge/ERC20Pool/ERC20PoolFactory.t.sol +++ b/tests/forge/ERC20Pool/ERC20PoolFactory.t.sol @@ -3,6 +3,8 @@ pragma solidity 0.8.14; import { ERC20HelperContract } from './ERC20DSTestPlus.sol'; +import { Token } from '../utils/Tokens.sol'; + import { ERC20Pool } from 'src/ERC20Pool.sol'; import { ERC20PoolFactory } from 'src/ERC20PoolFactory.sol'; import { IPoolErrors } from 'src/interfaces/pool/commons/IPoolErrors.sol'; @@ -23,46 +25,38 @@ contract ERC20PoolFactoryTest is ERC20HelperContract { function testDeployERC20PoolWithZeroAddress() external { // should revert if trying to deploy with zero address as collateral - _assertDeployWith0xAddressRevert( - { - poolFactory: address(_poolFactory), - collateral: address(0), - quote: address(_quote), - interestRate: 0.05 * 10**18 - } - ); + _assertDeployWith0xAddressRevert({ + poolFactory: address(_poolFactory), + collateral: address(0), + quote: address(_quote), + interestRate: 0.05 * 10**18 + }); // should revert if trying to deploy with zero address as quote token - _assertDeployWith0xAddressRevert( - { - poolFactory: address(_poolFactory), - collateral: address(_collateral), - quote: address(0), - interestRate: 0.05 * 10**18 - } - ); + _assertDeployWith0xAddressRevert({ + poolFactory: address(_poolFactory), + collateral: address(_collateral), + quote: address(0), + interestRate: 0.05 * 10**18 + }); } function testDeployERC20PoolWithInvalidRate() external { // should revert if trying to deploy with interest rate lower than accepted - _assertDeployWithInvalidRateRevert( - { - poolFactory: address(_poolFactory), - collateral: address(_collateral), - quote: address(_quote), - interestRate: 10**18 - } - ); + _assertDeployWithInvalidRateRevert({ + poolFactory: address(_poolFactory), + collateral: address(_collateral), + quote: address(_quote), + interestRate: 10**18 + }); // should revert if trying to deploy with interest rate higher than accepted - _assertDeployWithInvalidRateRevert( - { - poolFactory: address(_poolFactory), - collateral: address(_collateral), - quote: address(_quote), - interestRate: 2 * 10**18 - } - ); + _assertDeployWithInvalidRateRevert({ + poolFactory: address(_poolFactory), + collateral: address(_collateral), + quote: address(_quote), + interestRate: 2 * 10**18 + }); // check tracking of deployed pools assertEq(_poolFactory.getDeployedPoolsList().length, 0); @@ -72,14 +66,12 @@ contract ERC20PoolFactoryTest is ERC20HelperContract { address poolOne = _poolFactory.deployPool(address(_collateral), address(_quote), 0.05 * 10**18); // should revert if trying to deploy same pool one more time - _assertDeployMultipleTimesRevert( - { - poolFactory: address(_poolFactory), - collateral: address(_collateral), - quote: address(_quote), - interestRate: 0.05 * 10**18 - } - ); + _assertDeployMultipleTimesRevert({ + poolFactory: address(_poolFactory), + collateral: address(_collateral), + quote: address(_quote), + interestRate: 0.05 * 10**18 + }); // should deploy different pool address poolTwo = _poolFactory.deployPool(address(_collateral), address(_collateral), 0.05 * 10**18); @@ -94,6 +86,22 @@ contract ERC20PoolFactoryTest is ERC20HelperContract { assertEq(_poolFactory.deployedPoolsList(1), poolTwo); } + function testDeployERC20PoolWithMinRate() external { + _poolFactory.deployPool(address(new Token("Collateral", "C1")), address(new Token("Quote", "Q1")), 0.01 * 10**18); + + // check tracking of deployed pools + assertEq(_poolFactory.getDeployedPoolsList().length, 1); + assertEq(_poolFactory.getNumberOfDeployedPools(), 1); + } + + function testDeployERC20PoolWithMaxRate() external { + _poolFactory.deployPool(address(new Token("Collateral", "C1")), address(new Token("Quote", "Q1")), 0.1 * 10**18); + + // check tracking of deployed pools + assertEq(_poolFactory.getDeployedPoolsList().length, 1); + assertEq(_poolFactory.getNumberOfDeployedPools(), 1); + } + function testDeployERC20Pool() external { skip(333); diff --git a/tests/forge/ERC20Pool/ERC20PoolFlashloan.t.sol b/tests/forge/ERC20Pool/ERC20PoolFlashloan.t.sol new file mode 100644 index 000000000..a316f48ad --- /dev/null +++ b/tests/forge/ERC20Pool/ERC20PoolFlashloan.t.sol @@ -0,0 +1,266 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.14; + +import '@openzeppelin/contracts/token/ERC20/ERC20.sol'; + +import { ERC20HelperContract } from './ERC20DSTestPlus.sol'; +import { + FlashloanBorrower, + SomeDefiStrategy, + SomeDefiStrategyWithRepayment +} from '../utils/FlashloanBorrower.sol'; + +import 'src/libraries/helpers/PoolHelper.sol'; +import 'src/ERC20Pool.sol'; +import 'src/ERC20PoolFactory.sol'; + +import { IPoolErrors } from 'src/interfaces/pool/IPool.sol'; + +contract ERC20PoolFlashloanTest is ERC20HelperContract { + address internal _borrower; + address internal _lender; + uint internal _bucketId; + uint internal _bucketPrice; + + function setUp() external { + _lender = makeAddr("lender"); + _borrower = makeAddr("borrower"); + + _mintQuoteAndApproveTokens(_lender, 100_000 * 1e18); + _mintQuoteAndApproveTokens(_borrower, 5_000 * 1e18); + _mintCollateralAndApproveTokens(_borrower, 100 * 1e18); + + // lender adds liquidity + _bucketPrice = 502.433988063349232760 * 1e18; + _bucketId = _indexOf(_bucketPrice); + assertEq(_bucketId, 2909); + + _addInitialLiquidity({ + from: _lender, + amount: 100_000 * 1e18, + index: _bucketId + }); + + // borrower draws debt + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + amount: 100 * 1e18 + }); + _borrow({ + from: _borrower, + amount: 25_000 * 1e18, + indexLimit: _bucketId, + newLup: _bucketPrice + }); + + (uint256 poolDebt,,) = _pool.debtInfo(); + assertEq(poolDebt, 25_024.038461538461550000 * 1e18); + } + + function testCollateralFlashloan() external tearDown { + skip(1 days); + uint256 loanAmount = 100 * 1e18; + assertEq(_pool.maxFlashLoan(address(_collateral)), loanAmount); + + // Create an example defi strategy + SomeDefiStrategy strategy = new SomeDefiStrategy(_collateral); + deal(address(_collateral), address(strategy), 10 * 1e18); + + // Create a flashloan borrower contract which interacts with the strategy + bytes memory strategyCalldata = abi.encodeWithSignature("makeMoney(uint256)", loanAmount); + FlashloanBorrower flasher = new FlashloanBorrower(address(strategy), strategyCalldata); + + // Run the token approvals + changePrank(address(flasher)); + _collateral.approve(address(_pool), loanAmount); + _collateral.approve(address(strategy), loanAmount); + + // Use a flashloan to interact with the strategy + assertEq(_collateral.balanceOf(address(flasher)), 0); + assertTrue(!flasher.callbackInvoked()); + _pool.flashLoan(flasher, address(_collateral), loanAmount, new bytes(0)); + assertTrue(flasher.callbackInvoked()); + assertEq(_collateral.balanceOf(address(flasher)), 3.5 * 1e18); + } + + function testFlashloanFee() external tearDown { + uint256 loanAmount = 100 * 1e18; + + // Ensure there is no fee for quote token + uint256 fee = _pool.flashFee(address(_quote), loanAmount); + assertEq(fee, 0); + + // Ensure there is no fee for collateral + fee = _pool.flashFee(address(_collateral), loanAmount); + assertEq(fee, 0); + + // Ensure fee reverts for a random address which isn't a token + _assertFlashloanFeeRevertsForToken(makeAddr("nobody"), loanAmount); + } + + function testMaxFlashloan() external tearDown { + assertEq(_pool.maxFlashLoan(_pool.quoteTokenAddress()), 75_000 * 1e18); + assertEq(_pool.maxFlashLoan(_pool.collateralAddress()), 100 * 1e18); + assertEq(_pool.maxFlashLoan(makeAddr("nobody")), 0); + } + + function testCannotFlashloanMoreCollateralThanAvailable() external tearDown { + FlashloanBorrower flasher = new FlashloanBorrower(address(0), new bytes(0)); + + // Cannot flashloan less than pool size but more than available quote token + _assertFlashloanTooLargeRevert(flasher, _pool.quoteTokenAddress(), 90_000 * 1e18); + + // Cannot flashloan more collateral than pledged + _assertFlashloanTooLargeRevert(flasher, _pool.collateralAddress(), 150 * 1e18); + } + + function testCannotFlashloanNonToken() external tearDown { + FlashloanBorrower flasher = new FlashloanBorrower(address(0), new bytes(0)); + + // Cannot flashloan a random address which isn't a token + _assertFlashloanUnavailableForToken(flasher, makeAddr("nobody"), 1); + } + + function testCallbackFailure() external tearDown { + uint256 loanAmount = 100 * 1e18; + + // Create an example defi strategy + SomeDefiStrategy strategy = new SomeDefiStrategy(_collateral); + + // Create a flashloan borrower contract which invokes a non-existant method on the strategy + bytes memory strategyCalldata = abi.encodeWithSignature("missing()"); + FlashloanBorrower flasher = new FlashloanBorrower(address(strategy), strategyCalldata); + + // Run approvals + changePrank(address(flasher)); + _quote.approve(address(_pool), loanAmount); + + // Make a failed attempt to interact with the strategy + vm.expectRevert(IPoolErrors.FlashloanCallbackFailed.selector); + _pool.flashLoan(flasher, address(_collateral), loanAmount, new bytes(0)); + assertFalse(flasher.callbackInvoked()); + } + + function testIncorrectBalanceAfterFlashloanFailure() external tearDown { + skip(1 days); + uint256 loanAmount = 100 * 1e18; + assertEq(_pool.maxFlashLoan(address(_collateral)), loanAmount); + + // Create an example defi strategy that pays a fee to pool contract + SomeDefiStrategyWithRepayment strategy = new SomeDefiStrategyWithRepayment(_collateral, address(_pool)); + deal(address(_collateral), address(strategy), 10 * 1e18); + + // Create a flashloan borrower contract which interacts with the strategy + bytes memory strategyCalldata = abi.encodeWithSignature("makeMoney(uint256)", loanAmount); + FlashloanBorrower flasher = new FlashloanBorrower(address(strategy), strategyCalldata); + + // Run the token approvals + changePrank(address(flasher)); + _collateral.approve(address(_pool), loanAmount); + _collateral.approve(address(strategy), loanAmount); + + // should revert as the pool balance after flashloan is different than the initial balance + vm.expectRevert(IPoolErrors.FlashloanIncorrectBalance.selector); + _pool.flashLoan(flasher, address(_collateral), loanAmount, new bytes(0)); + } +} + +contract ERC20PoolFlashloanPrecisionTest is ERC20HelperContract { + + ERC20 WBTC = ERC20(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599); + ERC20 USDC = ERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + + address internal _borrower; + address internal _lender; + + function setUp() external { + _pool = ERC20Pool(new ERC20PoolFactory(_ajna).deployPool(address(WBTC), address(USDC), 0.05 * 10**18)); + + _borrower = makeAddr("borrower"); + _lender = makeAddr("lender"); + + deal(address(WBTC), _borrower, 10 * 1e8); + + deal(address(USDC), _lender, 10_000 * 1e6); + + vm.startPrank(_borrower); + WBTC.approve(address(_pool), 10 * 1e18); + + changePrank(_lender); + USDC.approve(address(_pool), 10_000 * 1e18); + + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2500 + }); + + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + amount: 10 * 1e18 + }); + } + + function testWbtcFlashloan() external tearDown { + skip(1 days); + uint256 loanAmount = 10 * 1e8; + assertEq(_pool.maxFlashLoan(address(WBTC)), 10 * 1e8); + + // Create an example defi strategy + SomeDefiStrategy strategy = new SomeDefiStrategy(WBTC); + deal(address(WBTC), address(strategy), 10 * 1e8); + + // Create a flashloan borrower contract which interacts with the strategy + bytes memory strategyCalldata = abi.encodeWithSignature("makeMoney(uint256)", loanAmount); + FlashloanBorrower flasher = new FlashloanBorrower(address(strategy), strategyCalldata); + + // Run the token approvals + changePrank(address(flasher)); + WBTC.approve(address(_pool), loanAmount); + WBTC.approve(address(strategy), loanAmount); + + // cannot flashloan more than available in pool (by specifying pool instead collateral precision) + vm.expectRevert('SafeERC20: low-level call failed'); + _pool.flashLoan(flasher, address(WBTC), 10 * 1e18, new bytes(0)); + + // Use a flashloan to interact with the strategy + assertEq(WBTC.balanceOf(address(flasher)), 0); + assertTrue(!flasher.callbackInvoked()); + _pool.flashLoan(flasher, address(WBTC), loanAmount, new bytes(0)); + assertTrue(flasher.callbackInvoked()); + assertEq(WBTC.balanceOf(address(flasher)), 0.35 * 1e8); + } + + function testUsdcFlashloan() external tearDown { + skip(1 days); + uint256 loanAmount = 10_000 * 1e6; + assertEq(_pool.maxFlashLoan(address(USDC)), loanAmount); + + // Create an example defi strategy which produces enough yield to pay the fee + SomeDefiStrategy strategy = new SomeDefiStrategy(USDC); + deal(address(USDC), address(strategy), 10_000 * 1e6); + + // Create a flashloan borrower contract which interacts with the strategy + bytes memory strategyCalldata = abi.encodeWithSignature("makeMoney(uint256)", loanAmount); + FlashloanBorrower flasher = new FlashloanBorrower(address(strategy), strategyCalldata); + + // Run approvals + changePrank(address(flasher)); + USDC.approve(address(_pool), loanAmount); + USDC.approve(address(strategy), loanAmount); + + // cannot flashloan more than available in pool (by specifying pool instead quote token precision) + vm.expectRevert('ERC20: transfer amount exceeds balance'); + _pool.flashLoan(flasher, address(USDC), 10_000 * 1e18, new bytes(0)); + + // Use a flashloan to interact with the strategy + assertEq(USDC.balanceOf(address(flasher)), 0); + assertTrue(!flasher.callbackInvoked()); + _pool.flashLoan(flasher, address(USDC), loanAmount, new bytes(0)); + assertTrue(flasher.callbackInvoked()); + assertEq(USDC.balanceOf(address(flasher)), 350 * 1e6); + } + +} \ No newline at end of file diff --git a/tests/forge/ERC20Pool/ERC20PoolGasLoadTest.t.sol b/tests/forge/ERC20Pool/ERC20PoolGasLoadTest.t.sol index 23e301817..bd028a147 100644 --- a/tests/forge/ERC20Pool/ERC20PoolGasLoadTest.t.sol +++ b/tests/forge/ERC20Pool/ERC20PoolGasLoadTest.t.sol @@ -45,15 +45,13 @@ contract ERC20PoolGasLoadTest is ERC20DSTestPlus { _mintQuoteAndApproveTokens(lender, 200_000 * 1e18); vm.startPrank(lender); - _pool.addQuoteToken(100_000 * 1e18, 7388 - i); - _pool.addQuoteToken(100_000 * 1e18, 1 + i); + _pool.addQuoteToken(100_000 * 1e18, 7388 - i, block.timestamp + 2 minutes); + _pool.addQuoteToken(100_000 * 1e18, 1 + i, block.timestamp + 2 minutes); vm.stopPrank(); _lenders.push(lender); - unchecked { - ++i; - } + unchecked { ++i; } } } @@ -69,9 +67,8 @@ contract ERC20PoolGasLoadTest is ERC20DSTestPlus { vm.stopPrank(); _borrowers.push(borrower); - unchecked { - ++i; - } + + unchecked { ++i; } } } @@ -105,9 +102,11 @@ contract ERC20PoolCommonActionsGasLoadTest is ERC20PoolGasLoadTest { vm.assume(borrowerId_ <= LOANS_COUNT); address borrower = _borrowers[borrowerId_]; + skip(15 hours); + vm.prank(borrower); - ERC20Pool(address(_pool)).repayDebt(borrower, 100 * 1e18, 0); + ERC20Pool(address(_pool)).repayDebt(borrower, 100 * 1e18, 0, borrower, MAX_FENWICK_INDEX); assertEq(_noOfLoans(), LOANS_COUNT); } @@ -116,11 +115,14 @@ contract ERC20PoolCommonActionsGasLoadTest is ERC20PoolGasLoadTest { assertEq(_noOfLoans(), LOANS_COUNT); vm.assume(borrowerId_ <= LOANS_COUNT); + skip(15 hours); + address borrower = _borrowers[borrowerId_]; (uint256 debt, , ) = _poolUtils.borrowerInfo(address(_pool), borrower); + vm.prank(borrower); - ERC20Pool(address(_pool)).repayDebt(borrower, debt, 0); + ERC20Pool(address(_pool)).repayDebt(borrower, debt, 0, borrower, MAX_FENWICK_INDEX); assertEq(_noOfLoans(), LOANS_COUNT - 1); } @@ -129,18 +131,20 @@ contract ERC20PoolCommonActionsGasLoadTest is ERC20PoolGasLoadTest { assertEq(_noOfLoans(), LOANS_COUNT); vm.assume(borrowerId_ <= LOANS_COUNT); + skip(15 hours); + address borrower = _borrowers[borrowerId_]; + vm.prank(borrower); - _drawDebtNoLupCheck( - { - from: borrower, - borrower: borrower, - amountToBorrow: 1_000 * 1e18, - limitIndex: 5000, - collateralToPledge: 0 - } - ); + + _drawDebtNoLupCheck({ + from: borrower, + borrower: borrower, + amountToBorrow: 1_000 * 1e18, + limitIndex: 5000, + collateralToPledge: 0 + }); assertEq(_noOfLoans(), LOANS_COUNT); } @@ -156,31 +160,33 @@ contract ERC20PoolCommonActionsGasLoadTest is ERC20PoolGasLoadTest { _mintCollateralAndApproveTokens(newBorrower, 2_000 * 1e18); vm.startPrank(newBorrower); + skip(15 hours); - _drawDebtNoLupCheck( - { - from: newBorrower, - borrower: newBorrower, - amountToBorrow: 0, - limitIndex: 0, - collateralToPledge: 1_000 * 1e18 - } - ); + + _drawDebtNoLupCheck({ + from: newBorrower, + borrower: newBorrower, + amountToBorrow: 0, + limitIndex: 0, + collateralToPledge: 1_000 * 1e18 + }); + + skip(15 hours); - _drawDebtNoLupCheck( - { - from: newBorrower, - borrower: newBorrower, - amountToBorrow: 1_000 * 1e18, - limitIndex: 5000, - collateralToPledge: 0 - } - ); + _drawDebtNoLupCheck({ + from: newBorrower, + borrower: newBorrower, + amountToBorrow: 1_000 * 1e18, + limitIndex: 5000, + collateralToPledge: 0 + }); + vm.stopPrank(); assertEq(_noOfLoans(), LOANS_COUNT + 1); vm.revertTo(snapshot); + assertEq(_noOfLoans(), LOANS_COUNT); } @@ -193,10 +199,12 @@ contract ERC20PoolCommonActionsGasLoadTest is ERC20PoolGasLoadTest { assertEq(_noOfLoans(), LOANS_COUNT); address borrower = _borrowers[i]; + vm.prank(borrower); - ERC20Pool(address(_pool)).repayDebt(borrower, 100 * 1e18, 0); + ERC20Pool(address(_pool)).repayDebt(borrower, 100 * 1e18, 0, borrower, MAX_FENWICK_INDEX); assertEq(_noOfLoans(), LOANS_COUNT); + vm.revertTo(snapshot); } @@ -213,10 +221,12 @@ contract ERC20PoolCommonActionsGasLoadTest is ERC20PoolGasLoadTest { address borrower = _borrowers[i]; (uint256 debt, , ) = _poolUtils.borrowerInfo(address(_pool), borrower); + vm.prank(borrower); - ERC20Pool(address(_pool)).repayDebt(borrower, debt, 0); + ERC20Pool(address(_pool)).repayDebt(borrower, debt, 0, borrower, MAX_FENWICK_INDEX); assertEq(_noOfLoans(), LOANS_COUNT - 1); + vm.revertTo(snapshot); } @@ -228,20 +238,23 @@ contract ERC20PoolCommonActionsGasLoadTest is ERC20PoolGasLoadTest { for (uint256 i; i < LOANS_COUNT; i++) { uint256 snapshot = vm.snapshot(); + skip(15 hours); + assertEq(_noOfLoans(), LOANS_COUNT); address borrower = _borrowers[i]; - _drawDebtNoLupCheck( - { - from: borrower, - borrower: borrower, - amountToBorrow: 1_000 * 1e18, - limitIndex: 5000, - collateralToPledge: 0 - } - ); + + _drawDebtNoLupCheck({ + from: borrower, + borrower: borrower, + amountToBorrow: 1_000 * 1e18, + limitIndex: 5000, + collateralToPledge: 0 + }); + assertEq(_noOfLoans(), LOANS_COUNT); + vm.revertTo(snapshot); } @@ -255,14 +268,19 @@ contract ERC20PoolCommonActionsGasLoadTest is ERC20PoolGasLoadTest { _mintQuoteAndApproveTokens(lender, 200_000 * 1e18); vm.startPrank(lender); + skip(15 hours); - _pool.addQuoteToken(10_000 * 1e18, index_); + _pool.addQuoteToken(10_000 * 1e18, index_, block.timestamp + 2 minutes); + skip(15 hours); _pool.removeQuoteToken(5_000 * 1e18, index_); + skip(15 hours); - _pool.moveQuoteToken(1_000 * 1e18, index_, index_ + 1); + _pool.moveQuoteToken(1_000 * 1e18, index_, index_ + 1, block.timestamp + 2 minutes); + skip(15 hours); _pool.removeQuoteToken(type(uint256).max, index_); + vm.stopPrank(); } @@ -272,16 +290,23 @@ contract ERC20PoolCommonActionsGasLoadTest is ERC20PoolGasLoadTest { for (uint256 i = 1; i < LENDERS; i++) { uint256 snapshot = vm.snapshot(); + vm.startPrank(lender); + skip(15 hours); - _pool.addQuoteToken(10_000 * 1e18, 7388 - i); + _pool.addQuoteToken(10_000 * 1e18, 7388 - i, block.timestamp + 2 minutes); + skip(15 hours); - _pool.addQuoteToken(10_000 * 1e18, 1 + i); + _pool.addQuoteToken(10_000 * 1e18, 1 + i, block.timestamp + 2 minutes); + skip(15 hours); _pool.removeQuoteToken(5_000 * 1e18, 7388 - i); + skip(15 hours); _pool.removeQuoteToken(type(uint256).max, 1 + i); + vm.stopPrank(); + vm.revertTo(snapshot); } } @@ -291,14 +316,19 @@ contract ERC20PoolCommonActionsGasLoadTest is ERC20PoolGasLoadTest { _mintQuoteAndApproveTokens(kicker, type(uint256).max); // mint enough to cover bonds vm.warp(100_000 days); + vm.startPrank(kicker); + for (uint256 i; i < LOANS_COUNT; i ++) { _pool.kick(_borrowers[i]); } + skip(2 hours); + for (uint256 i; i < LOANS_COUNT - 1; i ++) { ERC20Pool(address(_pool)).take(_borrowers[i], 100 * 1e18, kicker, new bytes(0)); } + vm.stopPrank(); } @@ -307,14 +337,19 @@ contract ERC20PoolCommonActionsGasLoadTest is ERC20PoolGasLoadTest { _mintQuoteAndApproveTokens(kicker, type(uint256).max); // mint enough to cover bonds vm.warp(100_000 days); + vm.startPrank(kicker); + for (uint256 i; i < LOANS_COUNT; i ++) { _pool.kick(_borrowers[LOANS_COUNT - 1 - i]); } + skip(2 hours); + for (uint256 i; i < LOANS_COUNT - 1; i ++) { ERC20Pool(address(_pool)).take(_borrowers[LOANS_COUNT - 1 - i], 100 * 1e18, kicker, new bytes(0)); } + vm.stopPrank(); } @@ -323,15 +358,21 @@ contract ERC20PoolCommonActionsGasLoadTest is ERC20PoolGasLoadTest { _mintQuoteAndApproveTokens(kicker, type(uint256).max); // mint enough to cover bonds vm.startPrank(kicker); - _pool.addQuoteToken(500_000_000_000_000 * 1e18, 3_000); + + _pool.addQuoteToken(500_000_000_000_000 * 1e18, 3_000, block.timestamp + 2 minutes); vm.warp(100_000 days); + _pool.kickWithDeposit(3_000); // worst case scenario, pool interest accrues + skip(80 hours); + _pool.settle(_borrowers[LOANS_COUNT - 1], 10); + // kick remaining loans with deposit to get average gas cost for (uint256 i; i < LOANS_COUNT - 1; i ++) { _pool.kickWithDeposit(3_000); } + vm.stopPrank(); } } @@ -359,19 +400,28 @@ contract ERC20PoolGasArbTakeLoadTest is ERC20PoolGasLoadTest { _mintQuoteAndApproveTokens(kicker, type(uint256).max); // mint enough to cover bonds vm.warp(100_000 days); + vm.startPrank(kicker); + for (uint256 i; i < LOANS_COUNT; i ++) { _pool.kick(_borrowers[i]); } + // add quote tokens in bucket to arb - _pool.addQuoteToken(100_000 * 1e18, 1_000); + _pool.addQuoteToken(100_000 * 1e18, 1_000, block.timestamp + 2 minutes); + vm.stopPrank(); assertEq(_noOfLoans(), 0); // assert all loans are kicked + skip(14 hours); + address taker = makeAddr("taker"); + vm.startPrank(taker); + _pool.bucketTake(_borrowers[0], depositTake_, 1_000); + vm.stopPrank(); } @@ -380,19 +430,28 @@ contract ERC20PoolGasArbTakeLoadTest is ERC20PoolGasLoadTest { _mintQuoteAndApproveTokens(kicker, type(uint256).max); // mint enough to cover bonds vm.warp(100_000 days); + vm.startPrank(kicker); + for (uint256 i; i < LOANS_COUNT; i ++) { _pool.kick(_borrowers[LOANS_COUNT - 1 - i]); } + // add quote tokens in bucket to arb - _pool.addQuoteToken(100_000 * 1e18, 1_000); + _pool.addQuoteToken(100_000 * 1e18, 1_000, block.timestamp + 2 minutes); + vm.stopPrank(); assertEq(_noOfLoans(), 0); // assert all loans are kicked + skip(14 hours); + address taker = makeAddr("taker"); + vm.startPrank(taker); + _pool.bucketTake(_borrowers[LOANS_COUNT - 1], depositTake_, 1_000); + vm.stopPrank(); } @@ -410,7 +469,9 @@ contract ERC20PoolGasArbTakeLoadTest is ERC20PoolGasLoadTest { _mintQuoteAndApproveTokens(lender, 200_000 * 1e18); vm.startPrank(lender); - _pool.addQuoteToken(200_000 * 1e18, 5000 - i); + + _pool.addQuoteToken(200_000 * 1e18, 5000 - i, block.timestamp + 2 minutes); + vm.stopPrank(); _lenders.push(lender); diff --git a/tests/forge/ERC20Pool/ERC20PoolInfoUtils.t.sol b/tests/forge/ERC20Pool/ERC20PoolInfoUtils.t.sol index 6053f0262..3d2994639 100644 --- a/tests/forge/ERC20Pool/ERC20PoolInfoUtils.t.sol +++ b/tests/forge/ERC20Pool/ERC20PoolInfoUtils.t.sol @@ -34,51 +34,41 @@ contract ERC20PoolInfoUtilsTest is ERC20HelperContract { _mintQuoteAndApproveTokens(_lender1, 200_000 * 1e18); // lender deposits 10000 DAI in 5 buckets each - _addLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: highest, - lpAward: 10_000 * 1e27, - newLup: MAX_PRICE - } - ); - _addLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: high, - lpAward: 10_000 * 1e27, - newLup: MAX_PRICE - } - ); - _addLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: med, - lpAward: 10_000 * 1e27, - newLup: MAX_PRICE - } - ); - _addLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: low, - lpAward: 10_000 * 1e27, - newLup: MAX_PRICE - } - ); - _addLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: lowest, - lpAward: 10_000 * 1e27, - newLup: MAX_PRICE - } - ); + _addLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: highest, + lpAward: 10_000 * 1e18, + newLup: MAX_PRICE + }); + _addLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: high, + lpAward: 10_000 * 1e18, + newLup: MAX_PRICE + }); + _addLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: med, + lpAward: 10_000 * 1e18, + newLup: MAX_PRICE + }); + _addLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: low, + lpAward: 10_000 * 1e18, + newLup: MAX_PRICE + }); + _addLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: lowest, + lpAward: 10_000 * 1e18, + newLup: MAX_PRICE + }); _drawDebt({ from: _borrower, @@ -114,12 +104,13 @@ contract ERC20PoolInfoUtilsTest is ERC20HelperContract { uint256 scale, uint256 exchangeRate ) = _poolUtils.bucketInfo(address(_pool), 5000); + assertEq(price, 0.014854015662334135 * 1e18); assertEq(quoteTokens, 0); assertEq(collateral, 0); assertEq(bucketLPs, 0); assertEq(scale, 1 * 1e18); - assertEq(exchangeRate, 1 * 1e27); + assertEq(exchangeRate, 1 * 1e18); ( price, @@ -132,9 +123,9 @@ contract ERC20PoolInfoUtilsTest is ERC20HelperContract { assertEq(price, 2_995.912459898389633881 * 1e18); assertEq(quoteTokens, 10_000 * 1e18); assertEq(collateral, 0); - assertEq(bucketLPs, 10_000 * 1e27); + assertEq(bucketLPs, 10_000 * 1e18); assertEq(scale, 1 * 1e18); - assertEq(exchangeRate, 1 * 1e27); + assertEq(exchangeRate, 1 * 1e18); } function testPoolInfoUtilsLoansInfo() external { @@ -161,6 +152,7 @@ contract ERC20PoolInfoUtilsTest is ERC20HelperContract { uint256 lup, uint256 lupIndex ) = _poolUtils.poolPricesInfo(address(_pool)); + assertEq(hpb, 3_010.892022197881557845 * 1e18); assertEq(hpbIndex, 2550); assertEq(htp, 210.201923076923077020 * 1e18); @@ -183,6 +175,7 @@ contract ERC20PoolInfoUtilsTest is ERC20HelperContract { uint256 auctionPrice, uint256 timeRemaining ) = _poolUtils.poolReservesInfo(address(_pool)); + assertEq(reserves, 20.192307692307702000 * 1e18); assertEq(claimableReserves, 0); assertEq(claimableReservesRemaining, 0); @@ -197,6 +190,7 @@ contract ERC20PoolInfoUtilsTest is ERC20HelperContract { uint256 poolActualUtilization, uint256 poolTargetUtilization ) = _poolUtils.poolUtilizationInfo(address(_pool)); + assertEq(poolMinDebtAmount, 2_102.019230769230770200 * 1e18); assertEq(poolCollateralization, 14.181637252165253251 * 1e18); assertEq(poolActualUtilization, 0.420403846153846154 * 1e18); @@ -220,46 +214,46 @@ contract ERC20PoolInfoUtilsTest is ERC20HelperContract { assertEq( _poolUtils.lpsToCollateral( address(_pool), - 100 * 1e27, + 100 * 1e18, high ), 0 ); + changePrank(_borrower2); - ERC20Pool(address(_pool)).addCollateral(10 * 1e18, high); + ERC20Pool(address(_pool)).addCollateral(10 * 1e18, high, block.timestamp + 5 minutes); assertEq( _poolUtils.lpsToCollateral( address(_pool), - 5 * 1e27, + 5 * 1e18, high ), 1668940620571264 ); assertEq( _poolUtils.lpsToCollateral( address(_pool), - 20 * 1e27, + 20 * 1e18, high ), 6675762482285055 ); - assertEq( _poolUtils.lpsToQuoteTokens( address(_pool), - 100 * 1e27, + 100 * 1e18, high ), 100000000000000000000 ); assertEq( _poolUtils.lpsToQuoteTokens( address(_pool), - 5 * 1e27, + 5 * 1e18, high ), 5000000000000000000 ); assertEq( _poolUtils.lpsToQuoteTokens( address(_pool), - 20 * 1e27, + 20 * 1e18, high ), 20000000000000000000 ); diff --git a/tests/forge/ERC20Pool/ERC20PoolInterestRateAndEMAs.t.sol b/tests/forge/ERC20Pool/ERC20PoolInterestRateAndEMAs.t.sol index 2fc2e2b44..0e4e3e4ff 100644 --- a/tests/forge/ERC20Pool/ERC20PoolInterestRateAndEMAs.t.sol +++ b/tests/forge/ERC20Pool/ERC20PoolInterestRateAndEMAs.t.sol @@ -14,58 +14,54 @@ contract ERC20PoolInterestRateTestAndEMAs is ERC20HelperContract { address internal _borrower; address internal _borrower2; + address internal _borrower3; address internal _lender; address internal _lender1; + address internal _lender2; function setUp() external { _borrower = makeAddr("borrower"); _borrower2 = makeAddr("borrower2"); + _borrower3 = makeAddr("borrower3"); _lender = makeAddr("lender"); _lender1 = makeAddr("_lender1"); + _lender2 = makeAddr("_lender2"); _mintCollateralAndApproveTokens(_borrower, 10_000 * 1e18); _mintCollateralAndApproveTokens(_borrower2, 200 * 1e18); + _mintCollateralAndApproveTokens(_borrower3, 1_000_000_000 * 1e18); _mintQuoteAndApproveTokens(_lender, 200_000 * 1e18); _mintQuoteAndApproveTokens(_lender1, 200_000 * 1e18); + _mintQuoteAndApproveTokens(_lender2, 100_000_000_000_000_000 * 1e18); } function testPoolInterestRateIncreaseDecrease() external tearDown { - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 2550 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 20_000 * 1e18, - index: 2551 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 20_000 * 1e18, - index: 2552 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 50_000 * 1e18, - index: 3900 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 4200 - } - ); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2550 + }); + _addInitialLiquidity({ + from: _lender, + amount: 20_000 * 1e18, + index: 2551 + }); + _addInitialLiquidity({ + from: _lender, + amount: 20_000 * 1e18, + index: 2552 + }); + _addInitialLiquidity({ + from: _lender, + amount: 50_000 * 1e18, + index: 3900 + }); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 4200 + }); skip(10 days); @@ -91,15 +87,13 @@ contract ERC20PoolInterestRateTestAndEMAs is ERC20HelperContract { vm.expectEmit(true, true, false, true); emit UpdateInterestRate(0.05 * 1e18, 0.055 * 1e18); - _drawDebtNoLupCheck( - { - from: _borrower, - borrower: _borrower, - amountToBorrow: 46_000 * 1e18, - limitIndex: 4_300, - collateralToPledge: 100 * 1e18 - } - ); + _drawDebtNoLupCheck({ + from: _borrower, + borrower: _borrower, + amountToBorrow: 46_000 * 1e18, + limitIndex: 4_300, + collateralToPledge: 100 * 1e18 + }); _assertPool( PoolParams({ @@ -118,12 +112,10 @@ contract ERC20PoolInterestRateTestAndEMAs is ERC20HelperContract { interestRateUpdate: _startTime + 10 days }) ); - _assertEMAs( - { - debtEma: 4_340.881358710158802477 * 1e18, - lupColEma: 28_103.845662221475161347 * 1e18 - } - ); + _assertEMAs({ + debtEma: 4_340.881358710158802477 * 1e18, + lupColEma: 28_103.845662221475161347 * 1e18 + }); skip(14 hours); @@ -154,16 +146,15 @@ contract ERC20PoolInterestRateTestAndEMAs is ERC20HelperContract { interestRateUpdate: _startTime + 10 days + 14 hours }) ); - _assertEMAs( - { - debtEma: 8_279.448467499588505755 * 1e18, - lupColEma: 53_558.163735316008374982 * 1e18 - } - ); + _assertEMAs({ + debtEma: 8_279.448467499588505755 * 1e18, + lupColEma: 53_558.163735316008374982 * 1e18 + }); vm.revertTo(snapshot); // repay entire loan deal(address(_quote), _borrower, _quote.balanceOf(_borrower) + 200 * 1e18); + _repayDebt({ from: _borrower, borrower: _borrower, @@ -190,40 +181,33 @@ contract ERC20PoolInterestRateTestAndEMAs is ERC20HelperContract { interestRateUpdate: _startTime + 10 days + 14 hours }) ); - _assertEMAs( - { - debtEma: 4_340.881358710158802477 * 1e18, - lupColEma: 28_103.845662221475161347 * 1e18 - } - ); + _assertEMAs({ + debtEma: 4_340.881358710158802477 * 1e18, + lupColEma: 28_103.845662221475161347 * 1e18 + }); } function testOverutilizedPoolInterestRateIncrease() external tearDown { // lender deposits 1000 - _addInitialLiquidity( - { - from: _lender, - amount: 1_000 * 1e18, - index: 3232 - } - ); + _addInitialLiquidity({ + from: _lender, + amount: 1_000 * 1e18, + index: 3232 + }); // borrower draws 9100 - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - amount: 1_500 * 1e18 - } - ); - _borrow( - { - from: _borrower, - amount: 995 * 1e18, - indexLimit: 3300, - newLup: 100.332368143282009890 * 1e18 - } - ); + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + amount: 1_500 * 1e18 + }); + _borrow({ + from: _borrower, + amount: 995 * 1e18, + indexLimit: 3300, + newLup: 100.332368143282009890 * 1e18 + }); + _assertPool( PoolParams({ htp: 0.663971153846153846 * 1e18, @@ -244,15 +228,15 @@ contract ERC20PoolInterestRateTestAndEMAs is ERC20HelperContract { // force an interest rate update skip(13 hours); - _addLiquidity( - { - from: _lender, - amount: 0, - index: 3232, - lpAward: 0, - newLup: 100.332368143282009890 * 1e18 - } - ); + + _addLiquidity({ + from: _lender, + amount: 0, + index: 3232, + lpAward: 0, + newLup: 100.332368143282009890 * 1e18 + }); + _assertPool( PoolParams({ htp: 0.664069695689831259 * 1e18, @@ -275,30 +259,26 @@ contract ERC20PoolInterestRateTestAndEMAs is ERC20HelperContract { function testPoolInterestRateDecrease() external tearDown { // lender makes an initial deposit skip(1 hours); - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 2873 - } - ); + + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2873 + }); + // borrower draws debt skip(2 hours); - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - amount: 10 * 1e18 - } - ); - _borrow( - { - from: _borrower, - amount: 5_000 * 1e18, - indexLimit: 3000, - newLup: 601.252968524772188572 * 1e18 - } - ); + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + amount: 10 * 1e18 + }); + _borrow({ + from: _borrower, + amount: 5_000 * 1e18, + indexLimit: 3000, + newLup: 601.252968524772188572 * 1e18 + }); _assertPool( PoolParams({ @@ -323,13 +303,11 @@ contract ERC20PoolInterestRateTestAndEMAs is ERC20HelperContract { vm.expectEmit(true, true, false, true); emit UpdateInterestRate(0.05 * 1e18, 0.045 * 1e18); - _addLiquidityNoEventCheck( - { - from: _lender1, - amount: 1_000 * 1e18, - index: 2873 - } - ); + _addLiquidityNoEventCheck({ + from: _lender1, + amount: 1_000 * 1e18, + index: 2873 + }); _assertPool( PoolParams({ @@ -351,25 +329,21 @@ contract ERC20PoolInterestRateTestAndEMAs is ERC20HelperContract { } function testMinInterestRate() external tearDown { - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: _i1505_26 - } - ); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: _i1505_26 + }); // pledge a tiny amount of collateral and draw a tiny amount of debt - _drawDebt( - { - from: _borrower, - borrower: _borrower, - amountToBorrow: 0.00001 * 1e18, - limitIndex: _i1505_26, - collateralToPledge: 0.00001 * 1e18, - newLup: _p1505_26 - } - ); + _drawDebt({ + from: _borrower, + borrower: _borrower, + amountToBorrow: 0.00001 * 1e18, + limitIndex: _i1505_26, + collateralToPledge: 0.00001 * 1e18, + newLup: _p1505_26 + }); // confirm interest rate starts out at 5% _assertPool( @@ -394,17 +368,15 @@ contract ERC20PoolInterestRateTestAndEMAs is ERC20HelperContract { while (i < 77) { // trigger an interest accumulation skip(12 hours); - _borrowZeroAmount( - { - from: _borrower, - amount: 0, - indexLimit: _i1505_26, - newLup: _p1505_26 - } - ); - unchecked { - ++i; - } + + _borrowZeroAmount({ + from: _borrower, + amount: 0, + indexLimit: _i1505_26, + newLup: _p1505_26 + }); + + unchecked { ++i; } } // show the rate bottoms out at 10 bps @@ -428,25 +400,21 @@ contract ERC20PoolInterestRateTestAndEMAs is ERC20HelperContract { } function testMaxInterestRate() external tearDown { - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: _i1505_26 - } - ); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: _i1505_26 + }); // pledge a lot of collateral, but draw a tiny amount of debt - _drawDebt( - { - from: _borrower, - borrower: _borrower, - amountToBorrow: 0.00001 * 1e18, - limitIndex: _i1505_26, - collateralToPledge: 10_000 * 1e18, - newLup: _p1505_26 - } - ); + _drawDebt({ + from: _borrower, + borrower: _borrower, + amountToBorrow: 0.00001 * 1e18, + limitIndex: _i1505_26, + collateralToPledge: 10_000 * 1e18, + newLup: _p1505_26 + }); // confirm interest rate starts out at 5% _assertPool( @@ -471,17 +439,15 @@ contract ERC20PoolInterestRateTestAndEMAs is ERC20HelperContract { while (i < 196) { // trigger an interest accumulation skip(12 hours); - _borrowZeroAmount( - { - from: _borrower, - amount: 0, - indexLimit: _i1505_26, - newLup: _p1505_26 - } - ); - unchecked { - ++i; - } + + _borrowZeroAmount({ + from: _borrower, + amount: 0, + indexLimit: _i1505_26, + newLup: _p1505_26 + }); + + unchecked { ++i; } } // show the rate maxed out at 50000% @@ -506,46 +472,36 @@ contract ERC20PoolInterestRateTestAndEMAs is ERC20HelperContract { function testPendingInflator() external tearDown { // add liquidity - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 2550 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 2552 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 4200 - } - ); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2550 + }); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2552 + }); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 4200 + }); skip(3600); // draw debt - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - amount: 50 * 1e18 - } - ); - _borrow( - { - from: _borrower, - amount: 15_000 * 1e18, - indexLimit: 4_300, - newLup: 2_981.007422784467321543 * 1e18 - } - ); + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + amount: 50 * 1e18 + }); + _borrow({ + from: _borrower, + amount: 15_000 * 1e18, + indexLimit: 4_300, + newLup: 2_981.007422784467321543 * 1e18 + }); _assertPool( PoolParams({ @@ -590,20 +546,16 @@ contract ERC20PoolInterestRateTestAndEMAs is ERC20HelperContract { function testPoolEMAAndTargetUtilizationUpdate() external tearDown { // add initial quote to the pool - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 3_010 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 2_995 - } - ); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 3_010 + }); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2_995 + }); _assertPool( PoolParams({ @@ -622,29 +574,23 @@ contract ERC20PoolInterestRateTestAndEMAs is ERC20HelperContract { interestRateUpdate: _startTime }) ); - _assertEMAs( - { - debtEma: 0, - lupColEma: 0 - } - ); + _assertEMAs({ + debtEma: 0, + lupColEma: 0 + }); // borrower 1 borrows 500 quote from the pool - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - amount: 50 * 1e18 - } - ); - _borrow( - { - from: _borrower, - amount: 500 * 1e18, - indexLimit: 3_010, - newLup: 327.188250324085203338 * 1e18 - } - ); + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + amount: 50 * 1e18 + }); + _borrow({ + from: _borrower, + amount: 500 * 1e18, + indexLimit: 3_010, + newLup: 327.188250324085203338 * 1e18 + }); _assertPool( PoolParams({ @@ -663,28 +609,22 @@ contract ERC20PoolInterestRateTestAndEMAs is ERC20HelperContract { interestRateUpdate: _startTime }) ); - _assertEMAs( - { - debtEma: 0, - lupColEma: 0 - } - ); + _assertEMAs({ + debtEma: 0, + lupColEma: 0 + }); - _pledgeCollateral( - { - from: _borrower2, - borrower: _borrower2, - amount: 50 * 1e18 - } - ); - _borrow( - { - from: _borrower2, - amount: 500 * 1e18, - indexLimit: 3_010, - newLup: 327.188250324085203338 * 1e18 - } - ); + _pledgeCollateral({ + from: _borrower2, + borrower: _borrower2, + amount: 50 * 1e18 + }); + _borrow({ + from: _borrower2, + amount: 500 * 1e18, + indexLimit: 3_010, + newLup: 327.188250324085203338 * 1e18 + }); _assertPool( PoolParams({ @@ -703,24 +643,20 @@ contract ERC20PoolInterestRateTestAndEMAs is ERC20HelperContract { interestRateUpdate: _startTime }) ); - _assertEMAs( - { - debtEma: 0, - lupColEma: 0 - } - ); + _assertEMAs({ + debtEma: 0, + lupColEma: 0 + }); skip(10 days); // borrower 1 borrows 500 quote from the pool - _borrow( - { - from: _borrower, - amount: 10 * 1e18, - indexLimit: 3_010, - newLup: 327.188250324085203338 * 1e18 - } - ); + _borrow({ + from: _borrower, + amount: 10 * 1e18, + indexLimit: 3_010, + newLup: 327.188250324085203338 * 1e18 + }); _assertPool( PoolParams({ @@ -739,11 +675,63 @@ contract ERC20PoolInterestRateTestAndEMAs is ERC20HelperContract { interestRateUpdate: _startTime }) ); - _assertEMAs( - { - debtEma: 95.440014344854493304 * 1e18, - lupColEma: 954.400143448544933043 * 1e18 - } + _assertEMAs({ + debtEma: 95.440014344854493304 * 1e18, + lupColEma: 954.400143448544933043 * 1e18 + }); + } + + function testAccruePoolInterestHtpGtMaxPrice() external tearDown { + _addLiquidityNoEventCheck({ + from: _lender2, + amount: 100_000_000_000_000_000 * 1e18, + index: 1 + }); + + _drawDebtNoLupCheck({ + from: _borrower3, + borrower: _borrower3, + amountToBorrow: 90_000_000_000_000_000 * 1e18, + limitIndex: 5000, + collateralToPledge: 90_100_000 * 1e18 + }); + + skip(100 days); + + assertGt(MAX_PRICE, _htp()); + + uint256 expectedPoolDebt = 91329091841208027.611736396814389869 * 1e18; + + _assertPool( + PoolParams({ + htp: 999850593.357807564705882353 * 1e18, + lup: 999969141.897027226245329498 * 1e18, + poolSize: 100_000_000_000_000_000 * 1e18, + pledgedCollateral: 90_100_000 * 1e18, + encumberedCollateral: 91_331_910.170696775095411340 * 1e18, + poolDebt: 91329091841208027.611736396814389869 * 1e18, + actualUtilization: 0, + targetUtilization: 1 * 1e18, + minDebtAmount: 9132909184120802.761173639681438987 * 1e18, + loans: 1, + maxBorrower: address(_borrower3), + interestRate: 0.05 * 1e18, + interestRateUpdate: _startTime + }) ); + + (uint256 poolDebt,,) = _pool.debtInfo(); + assertEq(poolDebt, expectedPoolDebt); + + // force accrue interest + _addLiquidityNoEventCheck({ + from: _lender2, + amount: 0, + index: 1 + }); + + // check that no interest earned if HTP is over the highest price bucket + (poolDebt,,) = _pool.debtInfo(); + assertEq(poolDebt, expectedPoolDebt); } } diff --git a/tests/forge/ERC20Pool/ERC20PoolLiquidationsArbTake.t.sol b/tests/forge/ERC20Pool/ERC20PoolLiquidationsArbTake.t.sol index a9efb84f3..c7b3ab308 100644 --- a/tests/forge/ERC20Pool/ERC20PoolLiquidationsArbTake.t.sol +++ b/tests/forge/ERC20Pool/ERC20PoolLiquidationsArbTake.t.sol @@ -28,75 +28,57 @@ contract ERC20PoolLiquidationsArbTakeTest is ERC20HelperContract { _mintCollateralAndApproveTokens(_lender1, 4 * 1e18); // Lender adds Quote token accross 5 prices - _addInitialLiquidity( - { - from: _lender, - amount: 2_000 * 1e18, - index: _i9_91 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 5_000 * 1e18, - index: _i9_81 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 11_000 * 1e18, - index: _i9_72 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 25_000 * 1e18, - index: _i9_62 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 30_000 * 1e18, - index: _i9_52 - } - ); - - // first borrower adds collateral token and borrows - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - amount: 2 * 1e18 - } - ); - _borrow( - { - from: _borrower, - amount: 19.25 * 1e18, - indexLimit: _i9_91, - newLup: 9.917184843435912074 * 1e18 - } - ); - - // second borrower adds collateral token and borrows - _pledgeCollateral( - { - from: _borrower2, - borrower: _borrower2, - amount: 1_000 * 1e18 - } - ); - _borrow( - { - from: _borrower2, - amount: 7_980 * 1e18, - indexLimit: _i9_72, - newLup: 9.721295865031779605 * 1e18 - } - ); + _addInitialLiquidity({ + from: _lender, + amount: 2_000 * 1e18, + index: _i9_91 + }); + _addInitialLiquidity({ + from: _lender, + amount: 5_000 * 1e18, + index: _i9_81 + }); + _addInitialLiquidity({ + from: _lender, + amount: 11_000 * 1e18, + index: _i9_72 + }); + _addInitialLiquidity({ + from: _lender, + amount: 25_000 * 1e18, + index: _i9_62 + }); + _addInitialLiquidity({ + from: _lender, + amount: 30_000 * 1e18, + index: _i9_52 + }); + + // first borrower pledge collateral and borrows + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + amount: 2 * 1e18 + }); + _borrow({ + from: _borrower, + amount: 19.25 * 1e18, + indexLimit: _i9_91, + newLup: 9.917184843435912074 * 1e18 + }); + + // second borrower adds collateral and borrows + _pledgeCollateral({ + from: _borrower2, + borrower: _borrower2, + amount: 1_000 * 1e18 + }); + _borrow({ + from: _borrower2, + amount: 7_980 * 1e18, + indexLimit: _i9_72, + newLup: 9.721295865031779605 * 1e18 + }); /*****************************/ /*** Assert pre-kick state ***/ @@ -119,43 +101,37 @@ contract ERC20PoolLiquidationsArbTakeTest is ERC20HelperContract { interestRateUpdate: _startTime }) ); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 19.268509615384615394 * 1e18, - borrowerCollateral: 2 * 1e18, - borrowert0Np: 10.115967548076923081 * 1e18, - borrowerCollateralization: 1.009034539679184679 * 1e18 - } - ); - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 7_987.673076923076926760 * 1e18, - borrowerCollateral: 1_000 * 1e18, - borrowert0Np: 8.471136974495192174 * 1e18, - borrowerCollateralization: 1.217037273735858713 * 1e18 - } - ); - _assertReserveAuction( - { - reserves: 7.691586538461542154 * 1e18, - claimableReserves : 0, - claimableReservesRemaining: 0, - auctionPrice: 0, - timeRemaining: 0 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 19.268509615384615394 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 10.115967548076923081 * 1e18, + borrowerCollateralization: 1.009034539679184679 * 1e18 + }); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 7_987.673076923076926760 * 1e18, + borrowerCollateral: 1_000 * 1e18, + borrowert0Np: 8.471136974495192174 * 1e18, + borrowerCollateralization: 1.217037273735858713 * 1e18 + }); + + _assertReserveAuction({ + reserves: 7.691586538461542154 * 1e18, + claimableReserves : 0, + claimableReservesRemaining: 0, + auctionPrice: 0, + timeRemaining: 0 + }); + assertEq(_quote.balanceOf(_lender), 47_000 * 1e18); // should revert if there's no auction started - _assertArbTakeNoAuctionRevert( - { - from: _lender, - borrower: _borrower, - index: _i9_91 - } - ); + _assertArbTakeNoAuctionRevert({ + from: _lender, + borrower: _borrower, + index: _i9_91 + }); // Skip to make borrower undercollateralized skip(100 days); @@ -176,27 +152,22 @@ contract ERC20PoolLiquidationsArbTakeTest is ERC20HelperContract { neutralPrice: 0 }) ); - - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 19.534277977147272573 * 1e18, - borrowerCollateral: 2 * 1e18, - borrowert0Np: 10.115967548076923081 * 1e18, - borrowerCollateralization: 0.995306391810796636 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 19.534277977147272573 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 10.115967548076923081 * 1e18, + borrowerCollateralization: 0.995306391810796636 * 1e18 + }); - _kick( - { - from: _lender, - borrower: _borrower, - debt: 19.778456451861613480 * 1e18, - collateral: 2 * 1e18, - bond: 0.195342779771472726 * 1e18, - transferAmount: 0.195342779771472726 * 1e18 - } - ); + _kick({ + from: _lender, + borrower: _borrower, + debt: 19.778456451861613480 * 1e18, + collateral: 2 * 1e18, + bond: 0.195342779771472726 * 1e18, + transferAmount: 0.195342779771472726 * 1e18 + }); _assertAuction( AuctionParams({ @@ -214,196 +185,163 @@ contract ERC20PoolLiquidationsArbTakeTest is ERC20HelperContract { neutralPrice: 10.255495938002318100 * 1e18 }) ); - - _assertKicker( - { - kicker: _lender, - claimable: 0, - locked: 0.195342779771472726 * 1e18 - } - ); + _assertKicker({ + kicker: _lender, + claimable: 0, + locked: 0.195342779771472726 * 1e18 + }); } function testArbTakeCollateralRestrict() external tearDown { skip(6.5 hours); - _assertLenderLpBalance( - { - lender: _taker, - index: _i9_91, - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: _i9_91, - lpBalance: 2_000 * 1e27, - depositTime: _startTime - } - ); - _assertBucket( - { - index: _i9_91, - lpBalance: 2_000 * 1e27, - collateral: 0, - deposit: 2_027.000651340490292000 * 1e18, - exchangeRate: 1.013500325670245146000000000 * 1e27 - } - ); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 19.779116873676490456 * 1e18, - borrowerCollateral: 2 * 1e18, - borrowert0Np: 10.115967548076923081 * 1e18, - borrowerCollateralization: 0.982985835729561629 * 1e18 - } - ); + _assertLenderLpBalance({ + lender: _taker, + index: _i9_91, + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: _lender, + index: _i9_91, + lpBalance: 2_000 * 1e18, + depositTime: _startTime + }); + _assertBucket({ + index: _i9_91, + lpBalance: 2_000 * 1e18, + collateral: 0, + deposit: 2_027.000651340490292000 * 1e18, + exchangeRate: 1.013500325670245146 * 1e18 + }); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 19.779116873676490456 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 10.115967548076923081 * 1e18, + borrowerCollateralization: 0.982985835729561629 * 1e18 + }); // add liquidity to accrue interest and update reserves before arb take - _addLiquidity( - { - from: _lender1, - amount: 1 * 1e18, - index: _i9_52, - lpAward: 0.999996826562080000190961519 * 1e27, - newLup: 9.721295865031779605 * 1e18 - } - ); - _assertBucket( - { - index: _i9_91, - lpBalance: 2_000 * 1e27, - collateral: 0, - deposit: 2_027.007083921634518000 * 1e18, - exchangeRate: 1.013503541960817259000000000 * 1e27 - } - ); - _assertReserveAuction( - { - reserves: 23.911413759224212224 * 1e18, - claimableReserves : 0, - claimableReservesRemaining: 0, - auctionPrice: 0, - timeRemaining: 0 - } - ); + _addLiquidity({ + from: _lender1, + amount: 1 * 1e18, + index: _i9_52, + lpAward: 0.99999682656208 * 1e18, + newLup: 9.721295865031779605 * 1e18 + }); + + _assertBucket({ + index: _i9_91, + lpBalance: 2_000 * 1e18, + collateral: 0, + deposit: 2_027.007083921634518000 * 1e18, + exchangeRate: 1.013503541960817259 * 1e18 + }); + _assertReserveAuction({ + reserves: 23.911413759224212224 * 1e18, + claimableReserves : 0, + claimableReservesRemaining: 0, + auctionPrice: 0, + timeRemaining: 0 + }); _assertAuction( - AuctionParams({ - borrower: _borrower, - active: true, - kicker: _lender, - bondSize: 0.195342779771472726 * 1e18, // should be the same after arb take, kicker will be rewarded with LPs - bondFactor: 0.01 * 1e18, - kickTime: block.timestamp - 6.5 hours, - kickMomp: 9.818751856078723036 * 1e18, - totalBondEscrowed: 0.195342779771472726 * 1e18, - auctionPrice: 7.251730722192532064 * 1e18, - debtInAuction: 19.779116873676490456 * 1e18, - thresholdPrice: 9.889558436838245228 * 1e18, - neutralPrice: 10.255495938002318100 * 1e18 - }) - ); - - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 19.779116873676490456 * 1e18, - borrowerCollateral: 2 * 1e18, - borrowert0Np: 10.115967548076923081 * 1e18, - borrowerCollateralization: 0.982985835729561629 * 1e18 - } + AuctionParams({ + borrower: _borrower, + active: true, + kicker: _lender, + bondSize: 0.195342779771472726 * 1e18, // should be the same after arb take, kicker will be rewarded with LPs + bondFactor: 0.01 * 1e18, + kickTime: block.timestamp - 6.5 hours, + kickMomp: 9.818751856078723036 * 1e18, + totalBondEscrowed: 0.195342779771472726 * 1e18, + auctionPrice: 7.251730722192532064 * 1e18, + debtInAuction: 19.779116873676490456 * 1e18, + thresholdPrice: 9.889558436838245228 * 1e18, + neutralPrice: 10.255495938002318100 * 1e18 + }) ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 19.779116873676490456 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 10.115967548076923081 * 1e18, + borrowerCollateralization: 0.982985835729561629 * 1e18 + }); // Amount is restricted by the collateral in the loan - _arbTake( - { - from: _taker, - borrower: _borrower, - kicker: _lender, - index: _i9_91, - collateralArbed: 2 * 1e18, - quoteTokenAmount: 14.503461444385064128 * 1e18, - bondChange: 0.145034614443850641 * 1e18, - isReward: true, - lpAwardTaker: 5.259881215780552826000000000 * 1e27, - lpAwardKicker: 0.143102227509983165000000000 * 1e27 - } - ); - - _assertLenderLpBalance( - { - lender: _taker, - index: _i9_91, - lpBalance: 5.259881215780552826000000000 * 1e27, - depositTime: _startTime + 100 days + 6.5 hours - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: _i9_91, - lpBalance: 2_000.143102227509983165000000000 * 1e27, // rewarded with LPs in bucket - depositTime: _startTime + 100 days + 6.5 hours - } - ); - _assertBucket( - { - index: _i9_91, - lpBalance: 2_005.402983443290535991000000000 * 1e27, - collateral: 2 * 1e18, - deposit: 2_012.648657091693304514 * 1e18, - exchangeRate: 1.013503541960817259000463129 * 1e27 - } - ); + _arbTake({ + from: _taker, + borrower: _borrower, + kicker: _lender, + index: _i9_91, + collateralArbed: 2 * 1e18, + quoteTokenAmount: 14.503461444385064128 * 1e18, + bondChange: 0.145034614443850641 * 1e18, + isReward: true, + lpAwardTaker: 5.259881215780552826 * 1e18, + lpAwardKicker: 0.143102227509983165 * 1e18 + }); + + _assertLenderLpBalance({ + lender: _taker, + index: _i9_91, + lpBalance: 5.259881215780552826 * 1e18, + depositTime: _startTime + 100 days + 6.5 hours + }); + _assertLenderLpBalance({ + lender: _lender, + index: _i9_91, + lpBalance: 2_000.143102227509983165 * 1e18, // rewarded with LPs in bucket + depositTime: _startTime + 100 days + 6.5 hours + }); + _assertBucket({ + index: _i9_91, + lpBalance: 2_005.402983443290535991 * 1e18, + collateral: 2 * 1e18, + deposit: 2_012.648657091693304514 * 1e18, + exchangeRate: 1.013503541960817259 * 1e18 + }); // reserves should remain the same after arb take - _assertReserveAuction( - { - reserves: 25.295951940381566551 * 1e18, - claimableReserves : 0, - claimableReservesRemaining: 0, - auctionPrice: 0, - timeRemaining: 0 - } - ); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 6.805228224892631302 * 1e18, - borrowerCollateral: 0, - borrowert0Np: 10.115967548076923081 * 1e18, - borrowerCollateralization: 0 - } - ); + _assertReserveAuction({ + reserves: 25.295951940381566551 * 1e18, + claimableReserves : 0, + claimableReservesRemaining: 0, + auctionPrice: 0, + timeRemaining: 0 + }); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 6.805228224892631302 * 1e18, + borrowerCollateral: 0, + borrowert0Np: 10.115967548076923081 * 1e18, + borrowerCollateralization: 0 + }); _assertAuction( - AuctionParams({ - borrower: _borrower, - active: true, - kicker: _lender, - bondSize: 0.195342779771472726 * 1e18, // bond size remains the same, kicker was rewarded with LPs - bondFactor: 0.01 * 1e18, - kickTime: block.timestamp - 6.5 hours, - kickMomp: 9.818751856078723036 * 1e18, - totalBondEscrowed: 0.195342779771472726 * 1e18, - auctionPrice: 7.251730722192532064 * 1e18, - debtInAuction: 6.805228224892631302 * 1e18, - thresholdPrice: 0, - neutralPrice: 10.255495938002318100 * 1e18 - }) + AuctionParams({ + borrower: _borrower, + active: true, + kicker: _lender, + bondSize: 0.195342779771472726 * 1e18, // bond size remains the same, kicker was rewarded with LPs + bondFactor: 0.01 * 1e18, + kickTime: block.timestamp - 6.5 hours, + kickMomp: 9.818751856078723036 * 1e18, + totalBondEscrowed: 0.195342779771472726 * 1e18, + auctionPrice: 7.251730722192532064 * 1e18, + debtInAuction: 6.805228224892631302 * 1e18, + thresholdPrice: 0, + neutralPrice: 10.255495938002318100 * 1e18 + }) ); // Arb take should fail on an auction without any remaining collateral to auction - _assertArbTakeInsufficentCollateralRevert( - { - from: _taker, - borrower: _borrower, - index: _i9_91 - } - ); + _assertArbTakeInsufficentCollateralRevert({ + from: _taker, + borrower: _borrower, + index: _i9_91 + }); } function testArbTakeDebtRestrict() external tearDown { @@ -411,103 +349,85 @@ contract ERC20PoolLiquidationsArbTakeTest is ERC20HelperContract { skip(5 hours); _assertAuction( - AuctionParams({ - borrower: _borrower, - active: true, - kicker: _lender, - bondSize: 0.195342779771472726 * 1e18, - bondFactor: 0.01 * 1e18, - kickTime: block.timestamp - 5 hours, - kickMomp: 9.818751856078723036 * 1e18, - totalBondEscrowed: 0.195342779771472726 * 1e18, - auctionPrice: 20.510991876004636192 * 1e18, - debtInAuction: 19.778456451861613480 * 1e18, - thresholdPrice: 9.889482233342512889 * 1e18, - neutralPrice: 10.255495938002318100 * 1e18 - }) + AuctionParams({ + borrower: _borrower, + active: true, + kicker: _lender, + bondSize: 0.195342779771472726 * 1e18, + bondFactor: 0.01 * 1e18, + kickTime: block.timestamp - 5 hours, + kickMomp: 9.818751856078723036 * 1e18, + totalBondEscrowed: 0.195342779771472726 * 1e18, + auctionPrice: 20.510991876004636192 * 1e18, + debtInAuction: 19.778456451861613480 * 1e18, + thresholdPrice: 9.889482233342512889 * 1e18, + neutralPrice: 10.255495938002318100 * 1e18 + }) ); - _addLiquidity( - { - from: _lender, - amount: 25_000 * 1e18, - index: _i1505_26, - lpAward: 25_000 * 1e27, - newLup: 1_505.263728469068226832 * 1e18 - } - ); + _addLiquidity({ + from: _lender, + amount: 25_000 * 1e18, + index: _i1505_26, + lpAward: 25_000 * 1e18, + newLup: 1_505.263728469068226832 * 1e18 + }); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 19.778964466685025779 * 1e18, - borrowerCollateral: 2 * 1e18, - borrowert0Np: 10.115967548076923081 * 1e18, - borrowerCollateralization: 152.208547722958917634 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 19.778964466685025779 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 10.115967548076923081 * 1e18, + borrowerCollateralization: 152.208547722958917634 * 1e18 + }); // Amount is restricted by the debt in the loan - _arbTake( - { - from: _taker, - borrower: _borrower, - kicker: _lender, - index: _i1505_26, - collateralArbed: 1.031812215971460994 * 1e18, - quoteTokenAmount: 21.163491979352977584 * 1e18, - bondChange: 0.195342779771472726 * 1e18, - isReward: false, - lpAwardTaker: 1_531.986011313779866428534379038 * 1e27, - lpAwardKicker: 0 - } - ); - - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 0, - borrowerCollateral: 0.968187784028539006 * 1e18, - borrowert0Np: 0, - borrowerCollateralization: 1 * 1e18 - } - ); - - _assertLenderLpBalance( - { - lender: _taker, - index: _i1505_26, - lpBalance: 1_531.986011313779866428534379038 * 1e27, - depositTime: block.timestamp - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: _i1505_26, - lpBalance: 25_000 * 1e27, - depositTime: block.timestamp - } - ); - _assertBucket( - { - index: _i1505_26, - lpBalance: 26_531.986011313779866428534379038 * 1e27, - collateral: 1.031812215971460994 * 1e18, - deposit: 24_978.836508020647022417 * 1e18, - exchangeRate: 0.999999999999999999999729424 * 1e27 - } - ); - - _assertReserveAuction( - { - reserves: 25.482262302272484525 * 1e18, - claimableReserves : 0, - claimableReservesRemaining: 0, - auctionPrice: 0, - timeRemaining: 0 - } - ); + _arbTake({ + from: _taker, + borrower: _borrower, + kicker: _lender, + index: _i1505_26, + collateralArbed: 1.031812215971460994 * 1e18, + quoteTokenAmount: 21.163491979352977584 * 1e18, + bondChange: 0.195342779771472726 * 1e18, + isReward: false, + lpAwardTaker: 1_531.986011313779866429 * 1e18, + lpAwardKicker: 0 + }); + + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 0, + borrowerCollateral: 0.968187784028539006 * 1e18, + borrowert0Np: 0, + borrowerCollateralization: 1 * 1e18 + }); + _assertLenderLpBalance({ + lender: _taker, + index: _i1505_26, + lpBalance: 1_531.986011313779866429 * 1e18, + depositTime: block.timestamp + }); + _assertLenderLpBalance({ + lender: _lender, + index: _i1505_26, + lpBalance: 25_000 * 1e18, + depositTime: block.timestamp + }); + _assertBucket({ + index: _i1505_26, + lpBalance: 26_531.986011313779866429 * 1e18, + collateral: 1.031812215971460994 * 1e18, + deposit: 24_978.836508020647022417 * 1e18, + exchangeRate: 1 * 1e18 + }); + _assertReserveAuction({ + reserves: 25.482262302272484525 * 1e18, + claimableReserves : 0, + claimableReservesRemaining: 0, + auctionPrice: 0, + timeRemaining: 0 + }); } function testArbTakeDepositRestrict() external tearDown { @@ -515,166 +435,127 @@ contract ERC20PoolLiquidationsArbTakeTest is ERC20HelperContract { skip(5 hours); _assertAuction( - AuctionParams({ - borrower: _borrower, - active: true, - kicker: _lender, - bondSize: 0.195342779771472726 * 1e18, - bondFactor: 0.01 * 1e18, - kickTime: block.timestamp - 5 hours, - kickMomp: 9.818751856078723036 * 1e18, - totalBondEscrowed: 0.195342779771472726 * 1e18, - auctionPrice: 20.510991876004636192 * 1e18, - debtInAuction: 19.778456451861613480 * 1e18, - thresholdPrice: 9.889482233342512889 * 1e18, - neutralPrice: 10.255495938002318100 * 1e18 - }) + AuctionParams({ + borrower: _borrower, + active: true, + kicker: _lender, + bondSize: 0.195342779771472726 * 1e18, + bondFactor: 0.01 * 1e18, + kickTime: block.timestamp - 5 hours, + kickMomp: 9.818751856078723036 * 1e18, + totalBondEscrowed: 0.195342779771472726 * 1e18, + auctionPrice: 20.510991876004636192 * 1e18, + debtInAuction: 19.778456451861613480 * 1e18, + thresholdPrice: 9.889482233342512889 * 1e18, + neutralPrice: 10.255495938002318100 * 1e18 + }) ); - _addLiquidity( - { - from: _lender, - amount: 15.0 * 1e18, - index: _i1505_26, - lpAward: 15.0 * 1e27, - newLup: 9.721295865031779605 * 1e18 - } - ); + _addLiquidity({ + from: _lender, + amount: 15.0 * 1e18, + index: _i1505_26, + lpAward: 15.0 * 1e18, + newLup: 9.721295865031779605 * 1e18 + }); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 19.778964466685025779 * 1e18, - borrowerCollateral: 2 * 1e18, - borrowert0Np: 10.115967548076923081 * 1e18, - borrowerCollateralization: 0.982993410135902682 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 19.778964466685025779 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 10.115967548076923081 * 1e18, + borrowerCollateralization: 0.982993410135902682 * 1e18 + }); // Amount is restricted by the deposit in the bucket - _arbTake( - { - from: _taker, - borrower: _borrower, - kicker: _lender, - index: _i1505_26, - collateralArbed: 0.731315193857015473 * 1e18, - quoteTokenAmount: 15.000000000000000000 * 1e18, - bondChange: 0.15 * 1e18, - isReward: false, - lpAwardTaker: 1_085.822235391290531116686016658 * 1e27, - lpAwardKicker: 0 - } - ); + _arbTake({ + from: _taker, + borrower: _borrower, + kicker: _lender, + index: _i1505_26, + collateralArbed: 0.731315193857015473 * 1e18, + quoteTokenAmount: 14.99999999999999999 * 1e18, + bondChange: 0.15 * 1e18, + isReward: false, + lpAwardTaker: 1_085.822235391290531090 * 1e18, + lpAwardKicker: 0 + }); _assertAuction( - AuctionParams({ - borrower: _borrower, - active: false, - kicker: address(0), - bondSize: 0, - bondFactor: 0, - kickTime: 0, - kickMomp: 0, - totalBondEscrowed: 0, - auctionPrice: 0, - debtInAuction: 0, - thresholdPrice: 4.858174346779663271 * 1e18, - neutralPrice: 0 - }) - ); - - _assertBucket( - { - index: _i1505_26, - lpBalance: 1_100.822235391290531116686016658 * 1e27, - collateral: 0.731315193857015473 * 1e18, - deposit: 0, - exchangeRate: 0.999999999999999999966196099 * 1e27 - } - ); - - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 6.163491979352977583 * 1e18, - borrowerCollateral: 1.268684806142984527 * 1e18, - borrowert0Np: 5.108498139847549815 * 1e18, - borrowerCollateralization: 2.001018319047304755 * 1e18 - } - ); - - _assertLenderLpBalance( - { - lender: _taker, - index: _i1505_26, - lpBalance: 1_085.822235391290531116686016658 * 1e27, - depositTime: block.timestamp - } - ); - - _assertLenderLpBalance( - { - lender: _lender, - index: _i1505_26, - lpBalance: 15.0 * 1e27, - depositTime: block.timestamp - } + AuctionParams({ + borrower: _borrower, + active: false, + kicker: address(0), + bondSize: 0, + bondFactor: 0, + kickTime: 0, + kickMomp: 0, + totalBondEscrowed: 0, + auctionPrice: 0, + debtInAuction: 0, + thresholdPrice: 4.858174346779663271 * 1e18, + neutralPrice: 0 + }) ); + _assertBucket({ + index: _i1505_26, + lpBalance: 1_100.822235391290531090 * 1e18, + collateral: 0.731315193857015473 * 1e18, + deposit: 0, + exchangeRate: 1 * 1e18 + }); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 6.163491979352977583 * 1e18, + borrowerCollateral: 1.268684806142984527 * 1e18, + borrowert0Np: 5.057793757429320955 * 1e18, + borrowerCollateralization: 2.001018319047304755 * 1e18 + }); + _assertLenderLpBalance({ + lender: _taker, + index: _i1505_26, + lpBalance: 1_085.822235391290531090 * 1e18, + depositTime: block.timestamp + }); + _assertLenderLpBalance({ + lender: _lender, + index: _i1505_26, + lpBalance: 15.0 * 1e18, + depositTime: block.timestamp + }); } function testArbTakeGTNeutralPrice() external tearDown { skip(3 hours); - _addLiquidity( - { - from: _lender, - amount: 1_000 * 1e18, - index: _i10016, - lpAward: 1_000 * 1e27, - newLup: 9.721295865031779605 * 1e18 - } - ); - - _assertLenderLpBalance( - { - lender: _taker, - index: _i10016, - lpBalance: 0, - depositTime: 0 - } - ); - - _assertLenderLpBalance( - { - lender: _lender, - index: _i10016, - lpBalance: 1_000 * 1e27, - depositTime: block.timestamp - } - ); - - _assertBucket( - { - index: _i10016, - lpBalance: 1_000 * 1e27, - collateral: 0, - deposit: 1_000 * 1e18, - exchangeRate: 1.0 * 1e27 - } - ); - - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 19.778761259189860403 * 1e18, - borrowerCollateral: 2 * 1e18, - borrowert0Np: 10.115967548076923081 * 1e18, - borrowerCollateralization: 0.983003509435146965 * 1e18 - } - ); - + _addLiquidity({ + from: _lender, + amount: 1_000 * 1e18, + index: _i10016, + lpAward: 1_000 * 1e18, + newLup: 9.721295865031779605 * 1e18 + }); + + _assertLenderLpBalance({ + lender: _taker, + index: _i10016, + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: _lender, + index: _i10016, + lpBalance: 1_000 * 1e18, + depositTime: block.timestamp + }); + _assertBucket({ + index: _i10016, + lpBalance: 1_000 * 1e18, + collateral: 0, + deposit: 1_000 * 1e18, + exchangeRate: 1.0 * 1e18 + }); _assertAuction( AuctionParams({ borrower: _borrower, @@ -691,73 +572,51 @@ contract ERC20PoolLiquidationsArbTakeTest is ERC20HelperContract { neutralPrice: 10.255495938002318100 * 1e18 }) ); - - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 19.778761259189860403 * 1e18, - borrowerCollateral: 2 * 1e18, - borrowert0Np: 10.115967548076923081 * 1e18, - borrowerCollateralization: 0.983003509435146965 * 1e18 - } - ); - - _arbTake( - { - from: _taker, - borrower: _borrower, - kicker: _lender, - index: _i10016, - collateralArbed: 0.257950403803869741 * 1e18, - quoteTokenAmount: 21.163274547333150631 * 1e18, - bondChange: 0.195342779771472726 * 1e18, - isReward: false, - lpAwardTaker: 2_562.597355112798042001349648580 * 1e27, - lpAwardKicker: 0 - } - ); - - _assertLenderLpBalance( - { - lender: _taker, - index: _i10016, - lpBalance: 2_562.597355112798042001349648580 * 1e27, // arb taker was rewarded LPBs in arbed bucket - depositTime: _startTime + 100 days + 3 hours - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: _i10016, - lpBalance: 1_000 * 1e27, - depositTime: _startTime + 100 days + 3 hours - } - ); - _assertKicker( - { - kicker: _lender, - claimable: 0, - locked: 0 // kicker was penalized - } - ); - _assertBucket( - { - index: _i10016, - lpBalance: 3_562.597355112798042001349648580 * 1e27, // LP balance in arbed bucket increased with LPs awarded for arb taker - collateral: 0.257950403803869741 * 1e18, // arbed collateral added to the arbed bucket - deposit: 978.836725452666849368 * 1e18, // quote token amount is diminished in arbed bucket - exchangeRate: 1.000000000000000000007160522 * 1e27 - } - ); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 0, - borrowerCollateral: 1.742049596196130259 * 1e18, - borrowert0Np: 0, - borrowerCollateralization: 1 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 19.778761259189860403 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 10.115967548076923081 * 1e18, + borrowerCollateralization: 0.983003509435146965 * 1e18 + }); + + _arbTake({ + from: _taker, + borrower: _borrower, + kicker: _lender, + index: _i10016, + collateralArbed: 0.257950403803869741 * 1e18, + quoteTokenAmount: 21.163274547333150631 * 1e18, + bondChange: 0.195342779771472726 * 1e18, + isReward: false, + lpAwardTaker: 2_562.597355112798042 * 1e18, + lpAwardKicker: 0 + }); + + _assertLenderLpBalance({ + lender: _taker, + index: _i10016, + lpBalance: 2_562.597355112798042 * 1e18, // arb taker was rewarded LPBs in arbed bucket + depositTime: _startTime + 100 days + 3 hours + }); + _assertLenderLpBalance({ + lender: _lender, + index: _i10016, + lpBalance: 1_000 * 1e18, + depositTime: _startTime + 100 days + 3 hours + }); + _assertKicker({ + kicker: _lender, + claimable: 0, + locked: 0 // kicker was penalized + }); + _assertBucket({ + index: _i10016, + lpBalance: 3_562.597355112798042 * 1e18, // LP balance in arbed bucket increased with LPs awarded for arb taker + collateral: 0.257950403803869741 * 1e18, // arbed collateral added to the arbed bucket + deposit: 978.836725452666849368 * 1e18, // quote token amount is diminished in arbed bucket + exchangeRate: 1 * 1e18 + }); _assertAuction( AuctionParams({ borrower: _borrower, @@ -774,27 +633,30 @@ contract ERC20PoolLiquidationsArbTakeTest is ERC20HelperContract { neutralPrice: 0 }) ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 0, + borrowerCollateral: 1.742049596196130259 * 1e18, + borrowert0Np: 0, + borrowerCollateralization: 1 * 1e18 + }); } function testArbTakeReverts() external tearDown { // should revert if borrower not auctioned - _assertArbTakeNoAuction( - { - from: _lender, - borrower: _borrower2, - index: _i9_91 - } - ); + _assertArbTakeNoAuction({ + from: _lender, + borrower: _borrower2, + index: _i9_91 + }); // should revert if auction in grace period - _assertArbTakeAuctionInCooldownRevert( - { - from: _lender, - borrower: _borrower, - index: _i9_91 - } - ); + _assertArbTakeAuctionInCooldownRevert({ + from: _lender, + borrower: _borrower, + index: _i9_91 + }); skip(2.5 hours); @@ -814,49 +676,40 @@ contract ERC20PoolLiquidationsArbTakeTest is ERC20HelperContract { neutralPrice: 10.255495938002318100 * 1e18 }) ); - - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 19.778710457642278866 * 1e18, - borrowerCollateral: 2 * 1e18, - borrowert0Np: 10.115967548076923081 * 1e18, - borrowerCollateralization: 0.983006034276170567 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 19.778710457642278866 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 10.115967548076923081 * 1e18, + borrowerCollateralization: 0.983006034276170567 * 1e18 + }); // should revert if bucket deposit is 0 - _assertArbTakeAuctionInsufficientLiquidityRevert( - { - from: _taker, - borrower: _borrower, - index: _i100_33 - } - ); + _assertArbTakeAuctionInsufficientLiquidityRevert({ + from: _taker, + borrower: _borrower, + index: _i100_33 + }); // should revert if auction price is greater than the bucket price - _assertArbTakeAuctionPriceGreaterThanBucketPriceRevert( - { - from: _taker, - borrower: _borrower, - index: _i9_91 - } - ); + _assertArbTakeAuctionPriceGreaterThanBucketPriceRevert({ + from: _taker, + borrower: _borrower, + index: _i9_91 + }); skip(4 hours); // 10 borrowers draw debt to enable the min debt check for (uint i=0; i<10; ++i) { - _anonBorrowerDrawsDebt(1_000 * 1e18, 6_000 * 1e18, 7777); + _anonBorrowerDrawsDebt(1_000 * 1e18, 6_000 * 1e18, MAX_FENWICK_INDEX); } // should revert if auction leaves borrower with debt under minimum pool debt - _assertArbTakeDebtUnderMinPoolDebtRevert( - { - from: _taker, - borrower: _borrower, - index: _i9_91 - } - ); + _assertArbTakeDebtUnderMinPoolDebtRevert({ + from: _taker, + borrower: _borrower, + index: _i9_91 + }); } } diff --git a/tests/forge/ERC20Pool/ERC20PoolLiquidationsDepositTake.t.sol b/tests/forge/ERC20Pool/ERC20PoolLiquidationsDepositTake.t.sol index cdbf77375..5a1818a59 100644 --- a/tests/forge/ERC20Pool/ERC20PoolLiquidationsDepositTake.t.sol +++ b/tests/forge/ERC20Pool/ERC20PoolLiquidationsDepositTake.t.sol @@ -28,75 +28,57 @@ contract ERC20PoolLiquidationsDepositTakeTest is ERC20HelperContract { _mintCollateralAndApproveTokens(_lender1, 4 * 1e18); // Lender adds Quote token accross 5 prices - _addInitialLiquidity( - { - from: _lender, - amount: 2_000 * 1e18, - index: _i9_91 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 5_000 * 1e18, - index: _i9_81 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 11_000 * 1e18, - index: _i9_72 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 25_000 * 1e18, - index: _i9_62 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 30_000 * 1e18, - index: _i9_52 - } - ); + _addInitialLiquidity({ + from: _lender, + amount: 2_000 * 1e18, + index: _i9_91 + }); + _addInitialLiquidity({ + from: _lender, + amount: 5_000 * 1e18, + index: _i9_81 + }); + _addInitialLiquidity({ + from: _lender, + amount: 11_000 * 1e18, + index: _i9_72 + }); + _addInitialLiquidity({ + from: _lender, + amount: 25_000 * 1e18, + index: _i9_62 + }); + _addInitialLiquidity({ + from: _lender, + amount: 30_000 * 1e18, + index: _i9_52 + }); // first borrower adds collateral token and borrows - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - amount: 2 * 1e18 - } - ); - _borrow( - { - from: _borrower, - amount: 19.25 * 1e18, - indexLimit: _i9_91, - newLup: 9.917184843435912074 * 1e18 - } - ); + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + amount: 2 * 1e18 + }); + _borrow({ + from: _borrower, + amount: 19.25 * 1e18, + indexLimit: _i9_91, + newLup: 9.917184843435912074 * 1e18 + }); // second borrower adds collateral token and borrows - _pledgeCollateral( - { - from: _borrower2, - borrower: _borrower2, - amount: 1_000 * 1e18 - } - ); - _borrow( - { - from: _borrower2, - amount: 7_980 * 1e18, - indexLimit: _i9_72, - newLup: 9.721295865031779605 * 1e18 - } - ); + _pledgeCollateral({ + from: _borrower2, + borrower: _borrower2, + amount: 1_000 * 1e18 + }); + _borrow({ + from: _borrower2, + amount: 7_980 * 1e18, + indexLimit: _i9_72, + newLup: 9.721295865031779605 * 1e18 + }); /*****************************/ /*** Assert pre-kick state ***/ @@ -119,43 +101,36 @@ contract ERC20PoolLiquidationsDepositTakeTest is ERC20HelperContract { interestRateUpdate: _startTime }) ); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 19.268509615384615394 * 1e18, - borrowerCollateral: 2 * 1e18, - borrowert0Np: 10.115967548076923081 * 1e18, - borrowerCollateralization: 1.009034539679184679 * 1e18 - } - ); - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 7_987.673076923076926760 * 1e18, - borrowerCollateral: 1_000 * 1e18, - borrowert0Np: 8.471136974495192174 * 1e18, - borrowerCollateralization: 1.217037273735858713 * 1e18 - } - ); - _assertReserveAuction( - { - reserves: 7.691586538461542154 * 1e18, - claimableReserves : 0, - claimableReservesRemaining: 0, - auctionPrice: 0, - timeRemaining: 0 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 19.268509615384615394 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 10.115967548076923081 * 1e18, + borrowerCollateralization: 1.009034539679184679 * 1e18 + }); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 7_987.673076923076926760 * 1e18, + borrowerCollateral: 1_000 * 1e18, + borrowert0Np: 8.471136974495192174 * 1e18, + borrowerCollateralization: 1.217037273735858713 * 1e18 + }); + _assertReserveAuction({ + reserves: 7.691586538461542154 * 1e18, + claimableReserves : 0, + claimableReservesRemaining: 0, + auctionPrice: 0, + timeRemaining: 0 + }); + assertEq(_quote.balanceOf(_lender), 47_000 * 1e18); // should revert if there's no auction started - _assertDepositTakeNoAuctionRevert( - { - from: _lender, - borrower: _borrower, - index: _i9_91 - } - ); + _assertDepositTakeNoAuctionRevert({ + from: _lender, + borrower: _borrower, + index: _i9_91 + }); // Skip to make borrower undercollateralized skip(250 days); @@ -176,27 +151,22 @@ contract ERC20PoolLiquidationsDepositTakeTest is ERC20HelperContract { neutralPrice: 0 }) ); - - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 19.939819504377940339 * 1e18, - borrowerCollateral: 2 * 1e18, - borrowert0Np: 10.115967548076923081 * 1e18, - borrowerCollateralization: 0.975063576969429891 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 19.939819504377940339 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 10.115967548076923081 * 1e18, + borrowerCollateralization: 0.975063576969429891 * 1e18 + }); - _kick( - { - from: _lender, - borrower: _borrower, - debt: 20.189067248182664593 * 1e18, - collateral: 2 * 1e18, - bond: 0.199398195043779403 * 1e18, - transferAmount: 0.199398195043779403 * 1e18 - } - ); + _kick({ + from: _lender, + borrower: _borrower, + debt: 20.189067248182664593 * 1e18, + collateral: 2 * 1e18, + bond: 0.199398195043779403 * 1e18, + transferAmount: 0.199398195043779403 * 1e18 + }); _assertAuction( AuctionParams({ @@ -214,195 +184,162 @@ contract ERC20PoolLiquidationsDepositTakeTest is ERC20HelperContract { neutralPrice: 10.468405239798418677 * 1e18 }) ); - - _assertKicker( - { - kicker: _lender, - claimable: 0, - locked: 0.199398195043779403 * 1e18 - } - ); + _assertKicker({ + kicker: _lender, + claimable: 0, + locked: 0.199398195043779403 * 1e18 + }); } function testDepositTakeCollateralRestrict() external tearDown { skip(6.5 hours); - _assertLenderLpBalance( - { - lender: _taker, - index: _i9_91, - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: _i9_91, - lpBalance: 2_000 * 1e27, - depositTime: _startTime - } - ); - _assertBucket( - { - index: _i9_91, - lpBalance: 2_000 * 1e27, - collateral: 0, - deposit: 2_000 * 1e18, - exchangeRate: 1 * 1e27 - } - ); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 20.189741380689676442 * 1e18, - borrowerCollateral: 2 * 1e18, - borrowert0Np: 10.115967548076923081 * 1e18, - borrowerCollateralization: 0.962993599742653326 * 1e18 - } - ); + _assertLenderLpBalance({ + lender: _taker, + index: _i9_91, + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: _lender, + index: _i9_91, + lpBalance: 2_000 * 1e18, + depositTime: _startTime + }); + _assertBucket({ + index: _i9_91, + lpBalance: 2_000 * 1e18, + collateral: 0, + deposit: 2_000 * 1e18, + exchangeRate: 1 * 1e18 + }); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 20.189741380689676442 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 10.115967548076923081 * 1e18, + borrowerCollateralization: 0.962993599742653326 * 1e18 + }); // add liquidity to accrue interest and update reserves before deposit take - _addLiquidity( - { - from: _lender1, - amount: 1 * 1e18, - index: _i9_52, - lpAward: 0.999996755983514720095749768 * 1e27, - newLup: 9.721295865031779605 * 1e18 - } - ); - _assertBucket( - { - index: _i9_91, - lpBalance: 2_000 * 1e27, - collateral: 0, - deposit: 2_000.006488054017914000 * 1e18, - exchangeRate: 1.000003244027008957000000000 * 1e27 - } - ); - _assertReserveAuction( - { - reserves: 286.940475866492567343 * 1e18, - claimableReserves : 245.508339417301835201 * 1e18, - claimableReservesRemaining: 0, - auctionPrice: 0, - timeRemaining: 0 - } - ); + _addLiquidity({ + from: _lender1, + amount: 1 * 1e18, + index: _i9_52, + lpAward: 0.999996755983514720 * 1e18, + newLup: 9.721295865031779605 * 1e18 + }); + + _assertBucket({ + index: _i9_91, + lpBalance: 2_000 * 1e18, + collateral: 0, + deposit: 2_000.006488054017914000 * 1e18, + exchangeRate: 1.000003244027008957 * 1e18 + }); + _assertReserveAuction({ + reserves: 286.940475866492567343 * 1e18, + claimableReserves : 245.508339417301835201 * 1e18, + claimableReservesRemaining: 0, + auctionPrice: 0, + timeRemaining: 0 + }); _assertAuction( - AuctionParams({ - borrower: _borrower, - active: true, - kicker: _lender, - bondSize: 0.199398195043779403 * 1e18, - bondFactor: 0.01 * 1e18, - kickTime: block.timestamp - 6.5 hours, - kickMomp: 9.818751856078723036 * 1e18, - totalBondEscrowed: 0.199398195043779403 * 1e18, - auctionPrice: 7.402280333270247968 * 1e18, - debtInAuction: 20.189741380689676442 * 1e18, - thresholdPrice: 10.094870690344838221 * 1e18, - neutralPrice: 10.468405239798418677 * 1e18 - }) - ); - - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 20.189741380689676442 * 1e18, - borrowerCollateral: 2 * 1e18, - borrowert0Np: 10.115967548076923081 * 1e18, - borrowerCollateralization: 0.962993599742653326 * 1e18 - } + AuctionParams({ + borrower: _borrower, + active: true, + kicker: _lender, + bondSize: 0.199398195043779403 * 1e18, + bondFactor: 0.01 * 1e18, + kickTime: block.timestamp - 6.5 hours, + kickMomp: 9.818751856078723036 * 1e18, + totalBondEscrowed: 0.199398195043779403 * 1e18, + auctionPrice: 7.402280333270247968 * 1e18, + debtInAuction: 20.189741380689676442 * 1e18, + thresholdPrice: 10.094870690344838221 * 1e18, + neutralPrice: 10.468405239798418677 * 1e18 + }) ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 20.189741380689676442 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 10.115967548076923081 * 1e18, + borrowerCollateralization: 0.962993599742653326 * 1e18 + }); // Amount is restricted by the collateral in the loan - _depositTake( - { - from: _taker, - borrower: _borrower, - kicker: _lender, - index: _i9_91, - collateralArbed: 2 * 1e18, - quoteTokenAmount: 19.834369686871824148 * 1e18, - bondChange: 0.198343696868718241 * 1e18, - isReward: true, - lpAwardTaker: 0, - lpAwardKicker: 0.198343053438495848000000000 * 1e27 - } - ); - - _assertLenderLpBalance( - { - lender: _taker, - index: _i9_91, - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: _i9_91, - lpBalance: 2_000.198343053438495848000000000 * 1e27, - depositTime: _startTime + 250 days + 6.5 hours - } - ); - _assertBucket( - { - index: _i9_91, - lpBalance: 2_000.198343053438495848000000000 * 1e27, - collateral: 2 * 1e18, - deposit: 1_980.370462064014808094 * 1e18, - exchangeRate: 1.000003244027008957000258924 * 1e27 - } - ); + _depositTake({ + from: _taker, + borrower: _borrower, + kicker: _lender, + index: _i9_91, + collateralArbed: 2 * 1e18, + quoteTokenAmount: 19.834369686871824148 * 1e18, + bondChange: 0.198343696868718241 * 1e18, + isReward: true, + lpAwardTaker: 0, + lpAwardKicker: 0.198343053438495848 * 1e18 + }); + + _assertLenderLpBalance({ + lender: _taker, + index: _i9_91, + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: _lender, + index: _i9_91, + lpBalance: 2_000.198343053438495848 * 1e18, + depositTime: _startTime + 250 days + 6.5 hours + }); + _assertBucket({ + index: _i9_91, + lpBalance: 2_000.198343053438495848 * 1e18, + collateral: 2 * 1e18, + deposit: 1_980.370462064014808094 * 1e18, + exchangeRate: 1.000003244027008957 * 1e18 + }); // reserves should remain the same after deposit take - _assertReserveAuction( - { - reserves: 288.353757763140844694 * 1e18, - claimableReserves : 247.012735034416886695 * 1e18, - claimableReservesRemaining: 0, - auctionPrice: 0, - timeRemaining: 0 - } - ); + _assertReserveAuction({ + reserves: 288.353757763140844694 * 1e18, + claimableReserves : 247.012735034416886695 * 1e18, + claimableReservesRemaining: 0, + auctionPrice: 0, + timeRemaining: 0 + }); _assertAuction( - AuctionParams({ - borrower: _borrower, - active: true, - kicker: _lender, - bondSize: 0.199398195043779403 * 1e18, - bondFactor: 0.01 * 1e18, - kickTime: block.timestamp - 6.5 hours, - kickMomp: 9.818751856078723036 * 1e18, - totalBondEscrowed: 0.199398195043779403 * 1e18, - auctionPrice: 7.402280333270247968 * 1e18, - debtInAuction: 1.966997287334847886 * 1e18, - thresholdPrice: 0, - neutralPrice: 10.468405239798418677 * 1e18 - }) + AuctionParams({ + borrower: _borrower, + active: true, + kicker: _lender, + bondSize: 0.199398195043779403 * 1e18, + bondFactor: 0.01 * 1e18, + kickTime: block.timestamp - 6.5 hours, + kickMomp: 9.818751856078723036 * 1e18, + totalBondEscrowed: 0.199398195043779403 * 1e18, + auctionPrice: 7.402280333270247968 * 1e18, + debtInAuction: 1.966997287334847886 * 1e18, + thresholdPrice: 0, + neutralPrice: 10.468405239798418677 * 1e18 + }) ); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 1.966997287334847886 * 1e18, - borrowerCollateral: 0, - borrowert0Np: 10.115967548076923081 * 1e18, - borrowerCollateralization: 0 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 1.966997287334847886 * 1e18, + borrowerCollateral: 0, + borrowert0Np: 10.115967548076923081 * 1e18, + borrowerCollateralization: 0 + }); // deposit take should fail on an auction without any remaining collateral to auction - _assertDepositTakeInsufficentCollateralRevert( - { - from: _taker, - borrower: _borrower, - index: _i9_91 - } - ); + _assertDepositTakeInsufficentCollateralRevert({ + from: _taker, + borrower: _borrower, + index: _i9_91 + }); } function testDepositTakeDebtRestrict() external tearDown { @@ -410,113 +347,92 @@ contract ERC20PoolLiquidationsDepositTakeTest is ERC20HelperContract { skip(5 hours); _assertAuction( - AuctionParams({ - borrower: _borrower, - active: true, - kicker: _lender, - bondSize: 0.199398195043779403 * 1e18, - bondFactor: 0.01 * 1e18, - kickTime: block.timestamp - 5 hours, - kickMomp: 9.818751856078723036 * 1e18, - totalBondEscrowed: 0.199398195043779403 * 1e18, - auctionPrice: 20.936810479596837344 * 1e18, - debtInAuction: 20.189067248182664592 * 1e18, - thresholdPrice: 10.094792904825850359 * 1e18, - neutralPrice: 10.468405239798418677 * 1e18 - }) - ); - - _addLiquidity( - { - from: _lender, - amount: 25_000 * 1e18, - index: _i1505_26, - lpAward: 25_000 * 1e27, - newLup: 1_505.263728469068226832 * 1e18 - } - ); - - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 20.189585809651700719 * 1e18, - borrowerCollateral: 2 * 1e18, - borrowert0Np: 10.115967548076923081 * 1e18, - borrowerCollateralization: 149.112888462473727465 * 1e18 - } + AuctionParams({ + borrower: _borrower, + active: true, + kicker: _lender, + bondSize: 0.199398195043779403 * 1e18, + bondFactor: 0.01 * 1e18, + kickTime: block.timestamp - 5 hours, + kickMomp: 9.818751856078723036 * 1e18, + totalBondEscrowed: 0.199398195043779403 * 1e18, + auctionPrice: 20.936810479596837344 * 1e18, + debtInAuction: 20.189067248182664592 * 1e18, + thresholdPrice: 10.094792904825850359 * 1e18, + neutralPrice: 10.468405239798418677 * 1e18 + }) ); - _assertBucket( - { - index: _i1505_26, - lpBalance: 25_000 * 1e27, - collateral: 0.0 * 1e18, - deposit: 25_000 * 1e18, - exchangeRate: 1.0 * 1e27 - } - ); + _addLiquidity({ + from: _lender, + amount: 25_000 * 1e18, + index: _i1505_26, + lpAward: 25_000 * 1e18, + newLup: 1_505.263728469068226832 * 1e18 + }); + + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 20.189585809651700719 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 10.115967548076923081 * 1e18, + borrowerCollateralization: 149.112888462473727465 * 1e18 + }); + _assertBucket({ + index: _i1505_26, + lpBalance: 25_000 * 1e18, + collateral: 0.0 * 1e18, + deposit: 25_000 * 1e18, + exchangeRate: 1.0 * 1e18 + }); // Amount is restricted by the debt in the loan - _depositTake( - { - from: _taker, - borrower: _borrower, - kicker: _lender, - index: _i1505_26, - collateralArbed: 0.014351542794629452 * 1e18, - quoteTokenAmount: 21.602856816327319769 * 1e18, - bondChange: 0.199398195043779403 * 1e18, - isReward: false, - lpAwardTaker: 0, - lpAwardKicker: 0 - } - ); - - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 0, - borrowerCollateral: 1.985648457205370548 * 1e18, - borrowert0Np: 0, - borrowerCollateralization: 1 * 1e18 - } - ); - - _assertLenderLpBalance( - { - lender: _taker, - index: _i1505_26, - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: _i1505_26, - lpBalance: 25_000 * 1e27, - depositTime: block.timestamp - } - ); - _assertBucket( - { - index: _i1505_26, - lpBalance: 25_000 * 1e27, - collateral: 0.014351542794629452 * 1e18, - deposit: 24_978.397143183672680231 * 1e18, - exchangeRate: 1.000000000000000000010323898 * 1e27 - } - ); - - _assertReserveAuction( - { - reserves: 288.543944498908261870 * 1e18, - claimableReserves : 247.213075232011850972 * 1e18, - claimableReservesRemaining: 0, - auctionPrice: 0, - timeRemaining: 0 - } - ); + _depositTake({ + from: _taker, + borrower: _borrower, + kicker: _lender, + index: _i1505_26, + collateralArbed: 0.014351542794629452 * 1e18, + quoteTokenAmount: 21.602856816327319769 * 1e18, + bondChange: 0.199398195043779403 * 1e18, + isReward: false, + lpAwardTaker: 0, + lpAwardKicker: 0 + }); + + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 0, + borrowerCollateral: 1.985648457205370548 * 1e18, + borrowert0Np: 0, + borrowerCollateralization: 1 * 1e18 + }); + _assertLenderLpBalance({ + lender: _taker, + index: _i1505_26, + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: _lender, + index: _i1505_26, + lpBalance: 25_000 * 1e18, + depositTime: block.timestamp + }); + _assertBucket({ + index: _i1505_26, + lpBalance: 25_000 * 1e18, + collateral: 0.014351542794629452 * 1e18, + deposit: 24_978.397143183672680231 * 1e18, + exchangeRate: 1 * 1e18 + }); + _assertReserveAuction({ + reserves: 288.543944498908261870 * 1e18, + claimableReserves : 247.213075232011850972 * 1e18, + claimableReservesRemaining: 0, + auctionPrice: 0, + timeRemaining: 0 + }); } function testDepositTakeDepositRestrict() external tearDown { @@ -524,166 +440,127 @@ contract ERC20PoolLiquidationsDepositTakeTest is ERC20HelperContract { skip(5 hours); _assertAuction( - AuctionParams({ - borrower: _borrower, - active: true, - kicker: _lender, - bondSize: 0.199398195043779403 * 1e18, - bondFactor: 0.01 * 1e18, - kickTime: block.timestamp - 5 hours, - kickMomp: 9.818751856078723036 * 1e18, - totalBondEscrowed: 0.199398195043779403 * 1e18, - auctionPrice: 20.936810479596837344 * 1e18, - debtInAuction: 20.189067248182664592 * 1e18, - thresholdPrice: 10.094792904825850359 * 1e18, - neutralPrice: 10.468405239798418677 * 1e18 - }) + AuctionParams({ + borrower: _borrower, + active: true, + kicker: _lender, + bondSize: 0.199398195043779403 * 1e18, + bondFactor: 0.01 * 1e18, + kickTime: block.timestamp - 5 hours, + kickMomp: 9.818751856078723036 * 1e18, + totalBondEscrowed: 0.199398195043779403 * 1e18, + auctionPrice: 20.936810479596837344 * 1e18, + debtInAuction: 20.189067248182664592 * 1e18, + thresholdPrice: 10.094792904825850359 * 1e18, + neutralPrice: 10.468405239798418677 * 1e18 + }) ); - _addLiquidity( - { - from: _lender, - amount: 15.0 * 1e18, - index: _i1505_26, - lpAward: 15.0 * 1e27, - newLup: 9.721295865031779605 * 1e18 - } - ); + _addLiquidity({ + from: _lender, + amount: 15.0 * 1e18, + index: _i1505_26, + lpAward: 15.0 * 1e18, + newLup: 9.721295865031779605 * 1e18 + }); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 20.189585809651700719 * 1e18, - borrowerCollateral: 2 * 1e18, - borrowert0Np: 10.115967548076923081 * 1e18, - borrowerCollateralization: 0.963001020098637267 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 20.189585809651700719 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 10.115967548076923081 * 1e18, + borrowerCollateralization: 0.963001020098637267 * 1e18 + }); // Amount is restricted by the deposit in the bucket in the loan - _depositTake( - { - from: _taker, - borrower: _borrower, - kicker: _lender, - index: _i1505_26, - collateralArbed: 0.009965031187761219 * 1e18, - quoteTokenAmount: 15.0 * 1e18, - bondChange: 0.15 * 1e18, - isReward: false, - lpAwardTaker: 0, - lpAwardKicker: 0 - } - ); + _depositTake({ + from: _taker, + borrower: _borrower, + kicker: _lender, + index: _i1505_26, + collateralArbed: 0.009965031187761219 * 1e18, + quoteTokenAmount: 14.999999999999999995 * 1e18, + bondChange: 0.15 * 1e18, + isReward: false, + lpAwardTaker: 0, + lpAwardKicker: 0 + }); _assertAuction( - AuctionParams({ - borrower: _borrower, - active: false, - kicker: address(0), - bondSize: 0, - bondFactor: 0, - kickTime: 0, - kickMomp: 0, - totalBondEscrowed: 0, - auctionPrice: 0, - debtInAuction: 0, - thresholdPrice: 3.317960196583009903 * 1e18, - neutralPrice: 0 - }) - ); - - _assertBucket( - { - index: _i1505_26, - lpBalance: 15 * 1e27, - collateral: 0.009965031187761219 * 1e18, - deposit: 0, - exchangeRate: 0.999999999999999999688877723 * 1e27 - } - ); - - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 6.602856816327319769 * 1e18, - borrowerCollateral: 1.990034968812238781 * 1e18, - borrowert0Np: 3.417963776167124997 * 1e18, - borrowerCollateralization: 2.929901291475173000 * 1e18 - } - ); - - _assertLenderLpBalance( - { - lender: _taker, - index: _i1505_26, - lpBalance: 0, - depositTime: 0 - } - ); - - _assertLenderLpBalance( - { - lender: _lender, - index: _i1505_26, - lpBalance: 15.0 * 1e27, - depositTime: block.timestamp - } + AuctionParams({ + borrower: _borrower, + active: false, + kicker: address(0), + bondSize: 0, + bondFactor: 0, + kickTime: 0, + kickMomp: 0, + totalBondEscrowed: 0, + auctionPrice: 0, + debtInAuction: 0, + thresholdPrice: 3.317960196583009903 * 1e18, + neutralPrice: 0 + }) ); + _assertBucket({ + index: _i1505_26, + lpBalance: 15 * 1e18, + collateral: 0.009965031187761219 * 1e18, + deposit: 0, + exchangeRate: 1 * 1e18 + }); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 6.602856816327319769 * 1e18, + borrowerCollateral: 1.990034968812238781 * 1e18, + borrowert0Np: 3.384038787324199948 * 1e18, + borrowerCollateralization: 2.929901291475173000 * 1e18 + }); + _assertLenderLpBalance({ + lender: _taker, + index: _i1505_26, + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: _lender, + index: _i1505_26, + lpBalance: 15.0 * 1e18, + depositTime: block.timestamp + }); } function testDepositTakeGTNeutralPrice() external tearDown { skip(3 hours); - _addLiquidity( - { - from: _lender, - amount: 1_000 * 1e18, - index: _i10016, - lpAward: 1_000 * 1e27, - newLup: 9.721295865031779605 * 1e18 - } - ); - - _assertLenderLpBalance( - { - lender: _taker, - index: _i10016, - lpBalance: 0, - depositTime: 0 - } - ); - - _assertLenderLpBalance( - { - lender: _lender, - index: _i10016, - lpBalance: 1_000 * 1e27, - depositTime: block.timestamp - } - ); - - _assertBucket( - { - index: _i10016, - lpBalance: 1_000 * 1e27, - collateral: 0, - deposit: 1_000 * 1e18, - exchangeRate: 1.0 * 1e27 - } - ); - - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 20.189378383465778990 * 1e18, - borrowerCollateral: 2 * 1e18, - borrowert0Np: 10.115967548076923081 * 1e18, - borrowerCollateralization: 0.963010913995558897 * 1e18 - } - ); - + _addLiquidity({ + from: _lender, + amount: 1_000 * 1e18, + index: _i10016, + lpAward: 1_000 * 1e18, + newLup: 9.721295865031779605 * 1e18 + }); + + _assertLenderLpBalance({ + lender: _taker, + index: _i10016, + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: _lender, + index: _i10016, + lpBalance: 1_000 * 1e18, + depositTime: block.timestamp + }); + _assertBucket({ + index: _i10016, + lpBalance: 1_000 * 1e18, + collateral: 0, + deposit: 1_000 * 1e18, + exchangeRate: 1.0 * 1e18 + }); _assertAuction( AuctionParams({ borrower: _borrower, @@ -700,74 +577,52 @@ contract ERC20PoolLiquidationsDepositTakeTest is ERC20HelperContract { neutralPrice: 10.468405239798418677 * 1e18 }) ); - - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 20.189378383465778990 * 1e18, - borrowerCollateral: 2 * 1e18, - borrowert0Np: 10.115967548076923081 * 1e18, - borrowerCollateralization: 0.963010913995558897 * 1e18 - } - ); - - _depositTake( - { - from: _taker, - borrower: _borrower, - kicker: _lender, - index: _i10016, - collateralArbed: 0.002156704581707556 * 1e18, - quoteTokenAmount: 21.602634870308383519 * 1e18, - bondChange: 0.199398195043779403 * 1e18, - isReward: false, - lpAwardTaker: 0, - lpAwardKicker: 0 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 20.189378383465778990 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 10.115967548076923081 * 1e18, + borrowerCollateralization: 0.963010913995558897 * 1e18 + }); + + _depositTake({ + from: _taker, + borrower: _borrower, + kicker: _lender, + index: _i10016, + collateralArbed: 0.002156704581707556 * 1e18, + quoteTokenAmount: 21.602634870308383519 * 1e18, + bondChange: 0.199398195043779403 * 1e18, + isReward: false, + lpAwardTaker: 0, + lpAwardKicker: 0 + }); // deposit taker wasn't rewarded any LPBs in arbed bucket - _assertLenderLpBalance( - { - lender: _taker, - index: _i10016, - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: _i10016, - lpBalance: 1_000 * 1e27, - depositTime: _startTime + 250 days + 3 hours - } - ); - _assertKicker( - { - kicker: _lender, - claimable: 0, - locked: 0 // kicker was penalized - } - ); - _assertBucket( - { - index: _i10016, - lpBalance: 1_000 * 1e27, // LP balance in arbed bucket increased with LPs awarded for deposit taker - collateral: 0.002156704581707556 * 1e18, // arbed collateral added to the arbed bucket - deposit: 978.397365129691616481* 1e18, // quote token amount is diminished in arbed bucket - exchangeRate: 0.999999999999999999966005802 * 1e27 - } - ); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 0, - borrowerCollateral: 1.997843295418292444 * 1e18, - borrowert0Np: 0, - borrowerCollateralization: 1 * 1e18 - } - ); + _assertLenderLpBalance({ + lender: _taker, + index: _i10016, + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: _lender, + index: _i10016, + lpBalance: 1_000 * 1e18, + depositTime: _startTime + 250 days + 3 hours + }); + _assertKicker({ + kicker: _lender, + claimable: 0, + locked: 0 // kicker was penalized + }); + _assertBucket({ + index: _i10016, + lpBalance: 1_000 * 1e18, // LP balance in arbed bucket increased with LPs awarded for deposit taker + collateral: 0.002156704581707556 * 1e18, // arbed collateral added to the arbed bucket + deposit: 978.397365129691616481* 1e18, // quote token amount is diminished in arbed bucket + exchangeRate: 1 * 1e18 + }); _assertAuction( AuctionParams({ borrower: _borrower, @@ -784,18 +639,24 @@ contract ERC20PoolLiquidationsDepositTakeTest is ERC20HelperContract { neutralPrice: 0 }) ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 0, + borrowerCollateral: 1.997843295418292444 * 1e18, + borrowert0Np: 0, + borrowerCollateralization: 1 * 1e18 + }); } function testDepositTakeReverts() external tearDown { // should revert if auction in grace period - _assertDepositTakeAuctionInCooldownRevert( - { - from: _lender, - borrower: _borrower, - index: _i9_91 - } - ); + _assertDepositTakeAuctionInCooldownRevert({ + from: _lender, + borrower: _borrower, + index: _i9_91 + } +); skip(2.5 hours); @@ -817,36 +678,31 @@ contract ERC20PoolLiquidationsDepositTakeTest is ERC20HelperContract { ); // should revert if bucket deposit is 0 - _assertDepositTakeAuctionInsufficientLiquidityRevert( - { - from: _taker, - borrower: _borrower, - index: _i100_33 - } - ); + _assertDepositTakeAuctionInsufficientLiquidityRevert({ + from: _taker, + borrower: _borrower, + index: _i100_33 + }); // should revert if auction price is greater than the bucket price - _assertDepositTakeAuctionPriceGreaterThanBucketPriceRevert( - { - from: _taker, - borrower: _borrower, - index: _i9_91 - } - ); + _assertDepositTakeAuctionPriceGreaterThanBucketPriceRevert({ + from: _taker, + borrower: _borrower, + index: _i9_91 + }); skip(4 hours); // 10 borrowers draw debt to enable the min debt check - for (uint i=0; i<10; ++i) { - _anonBorrowerDrawsDebt(1_000 * 1e18, 6_000 * 1e18, 7777); - } + for (uint256 i=0; i<10; ++i) { + _anonBorrowerDrawsDebt(1_000 * 1e18, 6_000 * 1e18, MAX_FENWICK_INDEX); + } + // should revert if auction leaves borrower with debt under minimum pool debt - _assertDepositTakeDebtUnderMinPoolDebtRevert( - { - from: _taker, - borrower: _borrower, - index: _i9_91 - } - ); + _assertDepositTakeDebtUnderMinPoolDebtRevert({ + from: _taker, + borrower: _borrower, + index: _i9_91 + }); } } diff --git a/tests/forge/ERC20Pool/ERC20PoolLiquidationsKick.t.sol b/tests/forge/ERC20Pool/ERC20PoolLiquidationsKick.t.sol index 7688a6d21..160c5591f 100644 --- a/tests/forge/ERC20Pool/ERC20PoolLiquidationsKick.t.sol +++ b/tests/forge/ERC20Pool/ERC20PoolLiquidationsKick.t.sol @@ -11,12 +11,14 @@ contract ERC20PoolLiquidationsKickTest is ERC20HelperContract { address internal _borrower2; address internal _lender; address internal _lender1; + address internal _withdrawRecipient; function setUp() external { - _borrower = makeAddr("borrower"); - _borrower2 = makeAddr("borrower2"); - _lender = makeAddr("lender"); - _lender1 = makeAddr("lender1"); + _borrower = makeAddr("borrower"); + _borrower2 = makeAddr("borrower2"); + _lender = makeAddr("lender"); + _lender1 = makeAddr("lender1"); + _withdrawRecipient = makeAddr("withdrawRecipient"); _mintQuoteAndApproveTokens(_lender, 120_000 * 1e18); _mintQuoteAndApproveTokens(_lender1, 120_000 * 1e18); @@ -26,75 +28,57 @@ contract ERC20PoolLiquidationsKickTest is ERC20HelperContract { _mintCollateralAndApproveTokens(_lender1, 4 * 1e18); // Lender adds Quote token accross 5 prices - _addInitialLiquidity( - { - from: _lender, - amount: 2_000 * 1e18, - index: _i9_91 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 5_000 * 1e18, - index: _i9_81 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 11_000 * 1e18, - index: _i9_72 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 25_000 * 1e18, - index: _i9_62 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 30_000 * 1e18, - index: _i9_52 - } - ); + _addInitialLiquidity({ + from: _lender, + amount: 2_000 * 1e18, + index: _i9_91 + }); + _addInitialLiquidity({ + from: _lender, + amount: 5_000 * 1e18, + index: _i9_81 + }); + _addInitialLiquidity({ + from: _lender, + amount: 11_000 * 1e18, + index: _i9_72 + }); + _addInitialLiquidity({ + from: _lender, + amount: 25_000 * 1e18, + index: _i9_62 + }); + _addInitialLiquidity({ + from: _lender, + amount: 30_000 * 1e18, + index: _i9_52 + }); // first borrower adds collateral token and borrows - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - amount: 2 * 1e18 - } - ); - _borrow( - { - from: _borrower, - amount: 19.25 * 1e18, - indexLimit: _i9_91, - newLup: 9.917184843435912074 * 1e18 - } - ); + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + amount: 2 * 1e18 + }); + _borrow({ + from: _borrower, + amount: 19.25 * 1e18, + indexLimit: _i9_91, + newLup: 9.917184843435912074 * 1e18 + }); // second borrower adds collateral token and borrows - _pledgeCollateral( - { - from: _borrower2, - borrower: _borrower2, - amount: 1_000 * 1e18 - } - ); - _borrow( - { - from: _borrower2, - amount: 7_980 * 1e18, - indexLimit: _i9_72, - newLup: 9.721295865031779605 * 1e18 - } - ); + _pledgeCollateral({ + from: _borrower2, + borrower: _borrower2, + amount: 1_000 * 1e18 + }); + _borrow({ + from: _borrower2, + amount: 7_980 * 1e18, + indexLimit: _i9_72, + newLup: 9.721295865031779605 * 1e18 + }); /*****************************/ /*** Assert pre-kick state ***/ @@ -117,33 +101,28 @@ contract ERC20PoolLiquidationsKickTest is ERC20HelperContract { interestRateUpdate: _startTime }) ); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 19.268509615384615394 * 1e18, - borrowerCollateral: 2 * 1e18, - borrowert0Np: 10.115967548076923081 * 1e18, - borrowerCollateralization: 1.009034539679184679 * 1e18 - } - ); - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 7_987.673076923076926760 * 1e18, - borrowerCollateral: 1_000 * 1e18, - borrowert0Np: 8.471136974495192174 * 1e18, - borrowerCollateralization: 1.217037273735858713 * 1e18 - } - ); - _assertReserveAuction( - { - reserves: 7.691586538461542154 * 1e18, - claimableReserves : 0, - claimableReservesRemaining: 0, - auctionPrice: 0, - timeRemaining: 0 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 19.268509615384615394 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 10.115967548076923081 * 1e18, + borrowerCollateralization: 1.009034539679184679 * 1e18 + }); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 7_987.673076923076926760 * 1e18, + borrowerCollateral: 1_000 * 1e18, + borrowert0Np: 8.471136974495192174 * 1e18, + borrowerCollateralization: 1.217037273735858713 * 1e18 + }); + _assertReserveAuction({ + reserves: 7.691586538461542154 * 1e18, + claimableReserves : 0, + claimableReservesRemaining: 0, + auctionPrice: 0, + timeRemaining: 0 + }); + assertEq(_quote.balanceOf(_lender), 47_000 * 1e18); } @@ -168,27 +147,22 @@ contract ERC20PoolLiquidationsKickTest is ERC20HelperContract { neutralPrice: 0 }) ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 19.534277977147272573 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 10.115967548076923081 * 1e18, + borrowerCollateralization: 0.995306391810796636 * 1e18 + }); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 19.534277977147272573 * 1e18, - borrowerCollateral: 2 * 1e18, - borrowert0Np: 10.115967548076923081 * 1e18, - borrowerCollateralization: 0.995306391810796636 * 1e18 - } - ); - - _kick( - { - from: _lender, - borrower: _borrower, - debt: 19.778456451861613480 * 1e18, - collateral: 2 * 1e18, - bond: 0.195342779771472726 * 1e18, - transferAmount: 0.195342779771472726 * 1e18 - } - ); + _kick({ + from: _lender, + borrower: _borrower, + debt: 19.778456451861613480 * 1e18, + collateral: 2 * 1e18, + bond: 0.195342779771472726 * 1e18, + transferAmount: 0.195342779771472726 * 1e18 + }); /******************************/ /*** Assert Post-kick state ***/ @@ -211,25 +185,23 @@ contract ERC20PoolLiquidationsKickTest is ERC20HelperContract { interestRateUpdate: block.timestamp }) ); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 19.778456451861613480 * 1e18, - borrowerCollateral: 2 * 1e18, - borrowert0Np: 10.115967548076923081 * 1e18, - borrowerCollateralization: 0.983018658578564579 * 1e18 - } - ); - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 8_097.846143253778448241 * 1e18, - borrowerCollateral: 1_000 * 1e18, - borrowert0Np: 8.471136974495192174 * 1e18, - borrowerCollateralization: 1.200479200648987171 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 19.778456451861613480 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 10.115967548076923081 * 1e18, + borrowerCollateralization: 0.983018658578564579 * 1e18 + }); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 8_097.846143253778448241 * 1e18, + borrowerCollateral: 1_000 * 1e18, + borrowert0Np: 8.471136974495192174 * 1e18, + borrowerCollateralization: 1.200479200648987171 * 1e18 + }); + assertEq(_quote.balanceOf(_lender), 46_999.804657220228527274 * 1e18); + _assertAuction( AuctionParams({ borrower: _borrower, @@ -246,55 +218,44 @@ contract ERC20PoolLiquidationsKickTest is ERC20HelperContract { neutralPrice: 10.255495938002318100 * 1e18 }) ); - _assertKicker( - { - kicker: _lender, - claimable: 0, - locked: 0.195342779771472726 * 1e18 - } - ); - _assertReserveAuction( - { - reserves: 23.872320013924039720 * 1e18, - claimableReserves : 0, - claimableReservesRemaining: 0, - auctionPrice: 0, - timeRemaining: 0 - } - ); + _assertKicker({ + kicker: _lender, + claimable: 0, + locked: 0.195342779771472726 * 1e18 + }); + _assertReserveAuction({ + reserves: 23.872320013924039720 * 1e18, + claimableReserves : 0, + claimableReservesRemaining: 0, + auctionPrice: 0, + timeRemaining: 0 + }); // kick should fail if borrower properly collateralized - _assertKickCollateralizedBorrowerRevert( - { - from: _lender, - borrower: _borrower2 - } - ); + _assertKickCollateralizedBorrowerRevert({ + from: _lender, + borrower: _borrower2 + }); - _assertDepositLockedByAuctionDebtRevert( - { - operator: _lender, - amount: 100 * 1e18, - index: _i9_91 - } - ); + _assertDepositLockedByAuctionDebtRevert({ + operator: _lender, + amount: 100 * 1e18, + index: _i9_91 + }); - // check locked pool actions if auction kicked for more than 72 hours and auction head not cleared skip(80 hours); - _assertRemoveLiquidityAuctionNotClearedRevert( - { - from: _lender, - amount: 1_000 * 1e18, - index: _i9_91 - } - ); - _assertRemoveCollateralAuctionNotClearedRevert( - { - from: _lender, - amount: 10 * 1e18, - index: _i9_91 - } - ); + + // check locked pool actions if auction kicked for more than 72 hours and auction head not cleared + _assertRemoveLiquidityAuctionNotClearedRevert({ + from: _lender, + amount: 1_000 * 1e18, + index: _i9_91 + }); + _assertRemoveCollateralAuctionNotClearedRevert({ + from: _lender, + amount: 10 * 1e18, + index: _i9_91 + }); } function testKickAndSaveByRepay() external tearDown { @@ -318,27 +279,23 @@ contract ERC20PoolLiquidationsKickTest is ERC20HelperContract { neutralPrice: 0 }) ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 19.534277977147272573 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 10.115967548076923081 * 1e18, + borrowerCollateralization: 0.995306391810796636 * 1e18 + }); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 19.534277977147272573 * 1e18, - borrowerCollateral: 2 * 1e18, - borrowert0Np: 10.115967548076923081 * 1e18, - borrowerCollateralization: 0.995306391810796636 * 1e18 - } - ); + _kick({ + from: _lender, + borrower: _borrower, + debt: 19.778456451861613480 * 1e18, + collateral: 2 * 1e18, + bond: 0.195342779771472726 * 1e18, + transferAmount: 0.195342779771472726 * 1e18 + }); - _kick( - { - from: _lender, - borrower: _borrower, - debt: 19.778456451861613480 * 1e18, - collateral: 2 * 1e18, - bond: 0.195342779771472726 * 1e18, - transferAmount: 0.195342779771472726 * 1e18 - } - ); _assertAuction( AuctionParams({ borrower: _borrower, @@ -355,25 +312,21 @@ contract ERC20PoolLiquidationsKickTest is ERC20HelperContract { neutralPrice: 10.255495938002318100 * 1e18 }) ); + _assertKicker({ + kicker: _lender, + claimable: 0, + locked: 0.195342779771472726 * 1e18 + }); - _assertKicker( - { - kicker: _lender, - claimable: 0, - locked: 0.195342779771472726 * 1e18 - } - ); + _repayAndSettleAuction({ + from: _borrower, + borrower: _borrower, + amount: 2 * 1e18, + repaid: 2 * 1e18, + collateral: 2 * 1e18, + newLup: 9.721295865031779605 * 1e18 + }); - _repayAndSettleAuction( - { - from: _borrower, - borrower: _borrower, - amount: 2 * 1e18, - repaid: 2 * 1e18, - collateral: 2 * 1e18, - newLup: 9.721295865031779605 * 1e18 - } - ); _assertAuction( AuctionParams({ borrower: _borrower, @@ -390,40 +343,35 @@ contract ERC20PoolLiquidationsKickTest is ERC20HelperContract { neutralPrice: 0 }) ); - _assertKicker( - { - kicker: _lender, - claimable: 0.195342779771472726 * 1e18, - locked: 0 - } - ); + _assertKicker({ + kicker: _lender, + claimable: 0.195342779771472726 * 1e18, + locked: 0 + }); // Skip to make borrower undercollateralized again skip(750 days); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 19.500754673204780610 * 1e18, - borrowerCollateral: 2 * 1e18, - borrowert0Np: 9.347497433934260033 * 1e18, - borrowerCollateralization: 0.997017400397270737 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 19.500754673204780610 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 9.254718877190426162 * 1e18, + borrowerCollateralization: 0.997017400397270737 * 1e18 + }); // Kick method only emit Kick event and doesn't call transfer method when kicker has enough bond amount in claimable - _kick( - { - from: _lender, - borrower: _borrower, - debt: 19.720138163278334392 * 1e18, - collateral: 2 * 1e18, - bond: 0.195007546732047806 * 1e18, - transferAmount: 0 - } - ); + _kick({ + from: _lender, + borrower: _borrower, + debt: 19.720138163278334392 * 1e18, + collateral: 2 * 1e18, + bond: 0.195007546732047806 * 1e18, + transferAmount: 0 + }); uint256 snapshot = vm.snapshot(); + // kicker not saved if partial debt paid only _repayDebt({ from: _borrower, @@ -444,12 +392,13 @@ contract ERC20PoolLiquidationsKickTest is ERC20HelperContract { kickTime: _startTime + 850 days, kickMomp: 9.818751856078723036 * 1e18, totalBondEscrowed: 0.195007546732047806 * 1e18, - auctionPrice: 332.622741621515951584 * 1e18, + auctionPrice: 329.321295632797165376 * 1e18, debtInAuction: 19.720038163278334392 * 1e18, thresholdPrice: 9.860019081639167196 * 1e18, - neutralPrice: 10.394460675672373487 * 1e18 + neutralPrice: 10.291290488524911418 * 1e18 }) ); + vm.revertTo(snapshot); // kicker saved if enough debt paid @@ -479,18 +428,31 @@ contract ERC20PoolLiquidationsKickTest is ERC20HelperContract { }) ); - // kicker withdraws his auction bonds + // kicker balance befor withdraw auction bonds + assertEq(_quote.balanceOf(_lender), 46_999.804657220228527274 * 1e18); + + snapshot = vm.snapshot(); + changePrank(_lender); + + // kicker withdraws auction bonds and transfer to a different address + _pool.withdrawBonds(_withdrawRecipient); + + assertEq(_quote.balanceOf(_withdrawRecipient), 0.195342779771472726 * 1e18); assertEq(_quote.balanceOf(_lender), 46_999.804657220228527274 * 1e18); - _pool.withdrawBonds(); + + vm.revertTo(snapshot); + + // kicker withdraws auction bonds + _pool.withdrawBonds(_lender); + assertEq(_quote.balanceOf(_lender), 47_000 * 1e18); - _assertKicker( - { - kicker: _lender, - claimable: 0, - locked: 0 - } - ); + + _assertKicker({ + kicker: _lender, + claimable: 0, + locked: 0 + }); } function testKickAndSaveByPledgeCollateral() external tearDown { @@ -514,27 +476,23 @@ contract ERC20PoolLiquidationsKickTest is ERC20HelperContract { neutralPrice: 0 }) ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 19.534277977147272573 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 10.115967548076923081 * 1e18, + borrowerCollateralization: 0.995306391810796636 * 1e18 + }); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 19.534277977147272573 * 1e18, - borrowerCollateral: 2 * 1e18, - borrowert0Np: 10.115967548076923081 * 1e18, - borrowerCollateralization: 0.995306391810796636 * 1e18 - } - ); + _kick({ + from: _lender, + borrower: _borrower, + debt: 19.778456451861613480 * 1e18, + collateral: 2 * 1e18, + bond: 0.195342779771472726 * 1e18, + transferAmount: 0.195342779771472726 * 1e18 + }); - _kick( - { - from: _lender, - borrower: _borrower, - debt: 19.778456451861613480 * 1e18, - collateral: 2 * 1e18, - bond: 0.195342779771472726 * 1e18, - transferAmount: 0.195342779771472726 * 1e18 - } - ); _assertAuction( AuctionParams({ borrower: _borrower, @@ -551,22 +509,19 @@ contract ERC20PoolLiquidationsKickTest is ERC20HelperContract { neutralPrice: 10.255495938002318100 * 1e18 }) ); - _assertKicker( - { - kicker: _lender, - claimable: 0, - locked: 0.195342779771472726 * 1e18 - } - ); + _assertKicker({ + kicker: _lender, + claimable: 0, + locked: 0.195342779771472726 * 1e18 + }); + + _pledgeCollateralAndSettleAuction({ + from: _borrower, + borrower: _borrower, + amount: 2 * 1e18, + collateral: 4 * 1e18 // collateral after auction settled = 2 new pledged + initial 2 collateral pledged + }); - _pledgeCollateralAndSettleAuction( - { - from: _borrower, - borrower: _borrower, - amount: 2 * 1e18, - collateral: 4 * 1e18 // collateral after auction settled = 2 new pledged + initial 2 collateral pledged - } - ); _assertAuction( AuctionParams({ borrower: _borrower, @@ -583,26 +538,25 @@ contract ERC20PoolLiquidationsKickTest is ERC20HelperContract { neutralPrice: 0 }) ); - _assertKicker( - { - kicker: _lender, - claimable: 0.195342779771472726 * 1e18, - locked: 0 - } - ); + _assertKicker({ + kicker: _lender, + claimable: 0.195342779771472726 * 1e18, + locked: 0 + }); // kicker withdraws his auction bonds changePrank(_lender); assertEq(_quote.balanceOf(_lender), 46_999.804657220228527274 * 1e18); - _pool.withdrawBonds(); + + _pool.withdrawBonds(_lender); + assertEq(_quote.balanceOf(_lender), 47_000 * 1e18); - _assertKicker( - { - kicker: _lender, - claimable: 0, - locked: 0 - } - ); + + _assertKicker({ + kicker: _lender, + claimable: 0, + locked: 0 + }); } function testKickActiveAuctionReverts() external tearDown { @@ -626,27 +580,23 @@ contract ERC20PoolLiquidationsKickTest is ERC20HelperContract { neutralPrice: 0 }) ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 19.534277977147272573 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 10.115967548076923081 * 1e18, + borrowerCollateralization: 0.995306391810796636 * 1e18 + }); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 19.534277977147272573 * 1e18, - borrowerCollateral: 2 * 1e18, - borrowert0Np: 10.115967548076923081 * 1e18, - borrowerCollateralization: 0.995306391810796636 * 1e18 - } - ); + _kick({ + from: _lender, + borrower: _borrower, + debt: 19.778456451861613480 * 1e18, + collateral: 2 * 1e18, + bond: 0.195342779771472726 * 1e18, + transferAmount: 0.195342779771472726 * 1e18 + }); - _kick( - { - from: _lender, - borrower: _borrower, - debt: 19.778456451861613480 * 1e18, - collateral: 2 * 1e18, - bond: 0.195342779771472726 * 1e18, - transferAmount: 0.195342779771472726 * 1e18 - } - ); _assertAuction( AuctionParams({ borrower: _borrower, @@ -665,92 +615,76 @@ contract ERC20PoolLiquidationsKickTest is ERC20HelperContract { ); // should not allow borrower to draw more debt if auction kicked - _assertBorrowAuctionActiveRevert( - { - from: _borrower, - amount: 1 * 1e18, - indexLimit: 7000 - } - ); + _assertBorrowAuctionActiveRevert({ + from: _borrower, + amount: 1 * 1e18, + indexLimit: 7000 + }); } function testInterestsAccumulationWithAllLoansAuctioned() external tearDown { // Borrower2 borrows - _borrow( - { - from: _borrower2, - amount: 1_730 * 1e18, - indexLimit: _i9_72, - newLup: 9.721295865031779605 * 1e18 - } - ); + _borrow({ + from: _borrower2, + amount: 1_730 * 1e18, + indexLimit: _i9_72, + newLup: 9.721295865031779605 * 1e18 + }); // Skip to make borrower undercollateralized skip(100 days); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 19.534277977147272573 * 1e18, - borrowerCollateral: 2 * 1e18, - borrowert0Np: 10.115967548076923081 * 1e18, - borrowerCollateralization: 0.995306391810796636 * 1e18 - } - ); - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 9_853.394241979221645666 * 1e18, - borrowerCollateral: 1_000 * 1e18, - borrowert0Np: 10.307611531622595991 * 1e18, - borrowerCollateralization: 0.986593617011217057 * 1e18 - } - ); - _assertLoans( - { - noOfLoans: 2, - maxBorrower: _borrower2, - maxThresholdPrice: 9.719336538461538466 * 1e18 - } - ); + + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 19.534277977147272573 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 10.115967548076923081 * 1e18, + borrowerCollateralization: 0.995306391810796636 * 1e18 + }); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 9_853.394241979221645666 * 1e18, + borrowerCollateral: 1_000 * 1e18, + borrowert0Np: 10.307611531622595991 * 1e18, + borrowerCollateralization: 0.986593617011217057 * 1e18 + }); + _assertLoans({ + noOfLoans: 2, + maxBorrower: _borrower2, + maxThresholdPrice: 9.719336538461538466 * 1e18 + }); // kick first loan - _kick( - { - from: _lender, - borrower: _borrower2, - debt: 9_976.561670003961916237 * 1e18, - collateral: 1_000 * 1e18, - bond: 98.533942419792216457 * 1e18, - transferAmount: 98.533942419792216457 * 1e18 - } - ); - _assertLoans( - { - noOfLoans: 1, - maxBorrower: _borrower, - maxThresholdPrice: 9.767138988573636287 * 1e18 - } - ); + _kick({ + from: _lender, + borrower: _borrower2, + debt: 9_976.561670003961916237 * 1e18, + collateral: 1_000 * 1e18, + bond: 98.533942419792216457 * 1e18, + transferAmount: 98.533942419792216457 * 1e18 + }); + + _assertLoans({ + noOfLoans: 1, + maxBorrower: _borrower, + maxThresholdPrice: 9.767138988573636287 * 1e18 + }); // kick 2nd loan - _kick( - { - from: _lender, - borrower: _borrower, - debt: 19.754038604390179389 * 1e18, - collateral: 2 * 1e18, - bond: 0.195342779771472726 * 1e18, - transferAmount: 0.195342779771472726 * 1e18 - } - ); - _assertLoans( - { - noOfLoans: 0, - maxBorrower: address(0), - maxThresholdPrice: 0 - } - ); + _kick({ + from: _lender, + borrower: _borrower, + debt: 19.754038604390179389 * 1e18, + collateral: 2 * 1e18, + bond: 0.195342779771472726 * 1e18, + transferAmount: 0.195342779771472726 * 1e18 + }); + _assertLoans({ + noOfLoans: 0, + maxBorrower: address(0), + maxThresholdPrice: 0 + }); _assertPool( PoolParams({ htp: 0, @@ -772,15 +706,13 @@ contract ERC20PoolLiquidationsKickTest is ERC20HelperContract { // force pool interest accumulation skip(14 hours); - _addLiquidity( - { - from: _lender1, - amount: 1 * 1e18, - index: _i9_91, - lpAward: 0.943930837199358257319707317 * 1e27, - newLup: 9.721295865031779605 * 1e18 - } - ); + _addLiquidity({ + from: _lender1, + amount: 1 * 1e18, + index: _i9_91, + lpAward: 0.943930837199358257 * 1e18, + newLup: 9.721295865031779605 * 1e18 + }); _assertPool( PoolParams({ diff --git a/tests/forge/ERC20Pool/ERC20PoolLiquidationsKickWithDeposit.t.sol b/tests/forge/ERC20Pool/ERC20PoolLiquidationsKickWithDeposit.t.sol index 7480b89e5..6bcd439fc 100644 --- a/tests/forge/ERC20Pool/ERC20PoolLiquidationsKickWithDeposit.t.sol +++ b/tests/forge/ERC20Pool/ERC20PoolLiquidationsKickWithDeposit.t.sol @@ -42,90 +42,72 @@ contract ERC20PoolLiquidationsKickWithDepositTest is ERC20HelperContract { _mintCollateralAndApproveTokens(_borrower5, 1_000 * 1e18); // Lender 1 adds Quote token accross 2 buckets - _addInitialLiquidity( - { - from: _lender1, - amount: 50_000 * 1e18, - index: 2500 - } - ); - _addInitialLiquidity( - { - from: _lender1, - amount: 50_000 * 1e18, - index: 2501 - } - ); - _addInitialLiquidity( - { - from: _lender1, - amount: 1_000 * 1e18, - index: 2502 - } - ); + _addInitialLiquidity({ + from: _lender1, + amount: 50_000 * 1e18, + index: 2500 + }); + _addInitialLiquidity({ + from: _lender1, + amount: 50_000 * 1e18, + index: 2501 + }); + _addInitialLiquidity({ + from: _lender1, + amount: 1_000 * 1e18, + index: 2502 + }); // all 5 borrowers draw debt from pool - _drawDebt( - { - from: _borrower1, - borrower: _borrower1, - amountToBorrow: 20_000 * 1e18, - limitIndex: 5000, - collateralToPledge: 1_000 * 1e18, - newLup: 3_863.654368867279344664 * 1e18 - } - ); - _drawDebt( - { - from: _borrower2, - borrower: _borrower2, - amountToBorrow: 20_000 * 1e18, - limitIndex: 5000, - collateralToPledge: 1_000 * 1e18, - newLup: 3_863.654368867279344664 * 1e18 - } - ); - _drawDebt( - { - from: _borrower3, - borrower: _borrower3, - amountToBorrow: 20_000 * 1e18, - limitIndex: 5000, - collateralToPledge: 1_000 * 1e18, - newLup: 3_844.432207828138682757 * 1e18 - } - ); - _drawDebt( - { - from: _borrower4, - borrower: _borrower4, - amountToBorrow: 20_000 * 1e18, - limitIndex: 5000, - collateralToPledge: 1_000 * 1e18, - newLup: 3_844.432207828138682757 * 1e18 - } - ); - _drawDebt( - { - from: _borrower5, - borrower: _borrower5, - amountToBorrow: 20_000 * 1e18, - limitIndex: 5000, - collateralToPledge: 1_000 * 1e18, - newLup: 3_825.305679430983794766 * 1e18 - } - ); + _drawDebt({ + from: _borrower1, + borrower: _borrower1, + amountToBorrow: 20_000 * 1e18, + limitIndex: 5000, + collateralToPledge: 1_000 * 1e18, + newLup: 3_863.654368867279344664 * 1e18 + }); + _drawDebt({ + from: _borrower2, + borrower: _borrower2, + amountToBorrow: 20_000 * 1e18, + limitIndex: 5000, + collateralToPledge: 1_000 * 1e18, + newLup: 3_863.654368867279344664 * 1e18 + }); + _drawDebt({ + from: _borrower3, + borrower: _borrower3, + amountToBorrow: 20_000 * 1e18, + limitIndex: 5000, + collateralToPledge: 1_000 * 1e18, + newLup: 3_844.432207828138682757 * 1e18 + }); + _drawDebt({ + from: _borrower4, + borrower: _borrower4, + amountToBorrow: 20_000 * 1e18, + limitIndex: 5000, + collateralToPledge: 1_000 * 1e18, + newLup: 3_844.432207828138682757 * 1e18 + }); + _drawDebt({ + from: _borrower5, + borrower: _borrower5, + amountToBorrow: 20_000 * 1e18, + limitIndex: 5000, + collateralToPledge: 1_000 * 1e18, + newLup: 3_825.305679430983794766 * 1e18 + }); // Lender 2 adds Quote token to top bucket - _addLiquidity( - { - from: _lender2, - amount: 10_000 * 1e18, - index: 2500, - lpAward: 10_000 * 1e27, - newLup: 3_844.432207828138682757 * 1e18 - } - ); + _addLiquidity({ + from: _lender2, + amount: 10_000 * 1e18, + index: 2500, + lpAward: 10_000 * 1e18, + newLup: 3_844.432207828138682757 * 1e18 + }); /*****************************/ /*** Assert pre-kick state ***/ @@ -148,6 +130,7 @@ contract ERC20PoolLiquidationsKickWithDepositTest is ERC20HelperContract { interestRateUpdate: _startTime }) ); + // assert balances assertEq(_quote.balanceOf(address(_pool)), 11_000 * 1e18); assertEq(_quote.balanceOf(_lender1), 49_000 * 1e18); @@ -159,13 +142,11 @@ contract ERC20PoolLiquidationsKickWithDepositTest is ERC20HelperContract { assertEq(_quote.balanceOf(_borrower5), 20_000 * 1e18); // assert lender cannot remove desired amount of 15000 quote tokens as LUP moves below HTP - _assertRemoveLiquidityLupBelowHtpRevert( - { - from: _lender1, - amount: 15_000 * 1e18, - index: 2500 - } - ); + _assertRemoveLiquidityLupBelowHtpRevert({ + from: _lender1, + amount: 15_000 * 1e18, + index: 2500 + }); } function testKickWithDepositAmountHigherThanAuctionBond() external tearDown { @@ -176,29 +157,25 @@ contract ERC20PoolLiquidationsKickWithDepositTest is ERC20HelperContract { */ // assert bucket state pre kick with deposit - _assertBucket( - { - index: 2500, - lpBalance: 60_000 * 1e27, - collateral: 0, - deposit: 60_000 * 1e18, - exchangeRate: 1 * 1e27 - } - ); + _assertBucket({ + index: 2500, + lpBalance: 60_000 * 1e18, + collateral: 0, + deposit: 60_000 * 1e18, + exchangeRate: 1 * 1e18 + }); - _kickWithDeposit( - { - from: _lender1, - index: 2500, - borrower: _borrower1, - debt: 20_269.471153846153855500 * 1e18, - collateral: 1_000 * 1e18, - bond: 6_005.769230769230772000 * 1e18, - removedFromDeposit: 6_005.769230769230772000 * 1e18, - transferAmount: 0, - lup: 3_844.432207828138682757 * 1e18 - } - ); + _kickWithDeposit({ + from: _lender1, + index: 2500, + borrower: _borrower1, + debt: 20_269.471153846153855500 * 1e18, + collateral: 1_000 * 1e18, + bond: 6_005.769230769230772000 * 1e18, + removedFromDeposit: 6_005.769230769230772000 * 1e18, + transferAmount: 0, + lup: 3_844.432207828138682757 * 1e18 + }); /******************************/ /*** Assert post-kick state ***/ @@ -221,6 +198,7 @@ contract ERC20PoolLiquidationsKickWithDepositTest is ERC20HelperContract { interestRateUpdate: _startTime }) ); + // assert balances - no change, bond was covered from deposit assertEq(_quote.balanceOf(address(_pool)), 11_000 * 1e18); assertEq(_quote.balanceOf(_lender1), 49_000 * 1e18); @@ -230,41 +208,34 @@ contract ERC20PoolLiquidationsKickWithDepositTest is ERC20HelperContract { assertEq(_quote.balanceOf(_borrower3), 20_000 * 1e18); assertEq(_quote.balanceOf(_borrower4), 20_000 * 1e18); assertEq(_quote.balanceOf(_borrower5), 20_000 * 1e18); - // assert lenders LPs in bucket used - _assertLenderLpBalance( - { - lender: _lender1, - index: 2500, - lpBalance: 43_994.230769230769228 * 1e27, // reduced by amount used to cover auction bond - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: _lender2, - index: 2500, - lpBalance: 10_000 * 1e27, - depositTime: _startTime - } - ); + + // assert lenders LPs in bucket used to kick + _assertLenderLpBalance({ + lender: _lender1, + index: 2500, + lpBalance: 43_994.230769230769228 * 1e18, // reduced by amount used to cover auction bond + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: _lender2, + index: 2500, + lpBalance: 10_000 * 1e18, + depositTime: _startTime + }); // assert bucket LPs - _assertBucket( - { - index: 2500, - lpBalance: 53_994.230769230769228 * 1e27, // reduced by amount used to cover auction bond - collateral: 0, - deposit: 53_994.230769230769228000 * 1e18, // reduced by amount used to cover auction bond - exchangeRate: 1 * 1e27 - } - ); + _assertBucket({ + index: 2500, + lpBalance: 53_994.230769230769228 * 1e18, // reduced by amount used to cover auction bond + collateral: 0, + deposit: 53_994.230769230769228000 * 1e18, // reduced by amount used to cover auction bond + exchangeRate: 1 * 1e18 + }); // assert lender1 as a kicker - _assertKicker( - { - kicker: _lender1, - claimable: 0, - locked: 6_005.769230769230772000 * 1e18 - } - ); + _assertKicker({ + kicker: _lender1, + claimable: 0, + locked: 6_005.769230769230772000 * 1e18 + }); // assert kicked auction _assertAuction( AuctionParams({ @@ -292,26 +263,23 @@ contract ERC20PoolLiquidationsKickWithDepositTest is ERC20HelperContract { */ // borrower 1 draws more debt from pool, bond size will increase from 6_005.769230769230772000 in prev scenario to 8_708.365384615384619400 - _drawDebt( - { - from: _borrower1, - borrower: _borrower1, - amountToBorrow: 9_000 * 1e18, - limitIndex: 5000, - collateralToPledge: 0, - newLup: 3_844.432207828138682757 * 1e18 - } - ); + _drawDebt({ + from: _borrower1, + borrower: _borrower1, + amountToBorrow: 9_000 * 1e18, + limitIndex: 5000, + collateralToPledge: 0, + newLup: 3_844.432207828138682757 * 1e18 + }); // Lender 3 adds collateral to top bucket - _addCollateral( - { - from: _lender3, - amount: 1 * 1e18, - index: 2500, - lpAward: 3_863.654368867279344664 * 1e27 // less than bond size - } - ); + _addCollateral({ + from: _lender3, + amount: 1 * 1e18, + index: 2500, + lpAward: 3_863.654368867279344664 * 1e18 // less than bond size + }); + // assert balances assertEq(_quote.balanceOf(address(_pool)), 2_000 * 1e18); assertEq(_quote.balanceOf(_lender1), 49_000 * 1e18); @@ -323,19 +291,17 @@ contract ERC20PoolLiquidationsKickWithDepositTest is ERC20HelperContract { assertEq(_quote.balanceOf(_borrower4), 20_000 * 1e18); assertEq(_quote.balanceOf(_borrower5), 20_000 * 1e18); - _kickWithDeposit( - { - from: _lender3, - index: 2500, - borrower: _borrower1, - debt: 29_390.733173076923090475 * 1e18, - collateral: 1_000 * 1e18, - bond: 8_708.365384615384619400 * 1e18, - removedFromDeposit: 3_863.654368867279344664 * 1e18, - transferAmount: 4_844.711015748105274736 * 1e18, - lup: 99836282890 - } - ); + _kickWithDeposit({ + from: _lender3, + index: 2500, + borrower: _borrower1, + debt: 29_390.733173076923090475 * 1e18, + collateral: 1_000 * 1e18, + bond: 8_708.365384615384619400 * 1e18, + removedFromDeposit: 3_863.654368867279344664 * 1e18, + transferAmount: 4_844.711015748105274736 * 1e18, + lup: 99836282890 + }); /******************************/ /*** Assert post-kick state ***/ @@ -358,6 +324,7 @@ contract ERC20PoolLiquidationsKickWithDepositTest is ERC20HelperContract { interestRateUpdate: _startTime }) ); + // assert balances assertEq(_quote.balanceOf(address(_pool)), 6_844.711015748105274736 * 1e18); // increased with the amount sent to cover bond assertEq(_quote.balanceOf(_lender1), 49_000 * 1e18); @@ -368,41 +335,34 @@ contract ERC20PoolLiquidationsKickWithDepositTest is ERC20HelperContract { assertEq(_quote.balanceOf(_borrower3), 20_000 * 1e18); assertEq(_quote.balanceOf(_borrower4), 20_000 * 1e18); assertEq(_quote.balanceOf(_borrower5), 20_000 * 1e18); + // assert lenders LPs in bucket used - _assertLenderLpBalance( - { - lender: _lender1, - index: 2500, - lpBalance: 50_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: _lender3, - index: 2500, - lpBalance: 0, - depositTime: _startTime - } - ); + _assertLenderLpBalance({ + lender: _lender1, + index: 2500, + lpBalance: 50_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: _lender3, + index: 2500, + lpBalance: 0, + depositTime: _startTime + }); // assert bucket LPs - _assertBucket( - { - index: 2500, - lpBalance: 60_000 * 1e27, - collateral: 1 * 1e18, - deposit: 56_136.345631132720655336 * 1e18, - exchangeRate: 1 * 1e27 - } - ); + _assertBucket({ + index: 2500, + lpBalance: 60_000 * 1e18, + collateral: 1 * 1e18, + deposit: 56_136.345631132720655336 * 1e18, + exchangeRate: 1 * 1e18 + }); // assert lender3 as a kicker - _assertKicker( - { - kicker: _lender3, - claimable: 0, - locked: 8_708.365384615384619400 * 1e18 - } - ); + _assertKicker({ + kicker: _lender3, + claimable: 0, + locked: 8_708.365384615384619400 * 1e18 + }); // assert kicked auction _assertAuction( AuctionParams({ @@ -429,26 +389,23 @@ contract ERC20PoolLiquidationsKickWithDepositTest is ERC20HelperContract { */ // lender 2 adds liquidity in new top bucket 2499 - _addLiquidity( - { - from: _lender2, - amount: 10_000 * 1e18, - index: 2499, - lpAward: 10_000 * 1e27, - newLup: 3_844.432207828138682757 * 1e18 - } - ); + _addLiquidity({ + from: _lender2, + amount: 10_000 * 1e18, + index: 2499, + lpAward: 10_000 * 1e18, + newLup: 3_844.432207828138682757 * 1e18 + }); + // borrower draws more debt consuming entire deposit from bucket 2499 - _drawDebt( - { - from: _borrower1, - borrower: _borrower1, - amountToBorrow: 15_000 * 1e18, - limitIndex: 5000, - collateralToPledge: 0, - newLup: 3_844.432207828138682757 * 1e18 - } - ); + _drawDebt({ + from: _borrower1, + borrower: _borrower1, + amountToBorrow: 15_000 * 1e18, + limitIndex: 5000, + collateralToPledge: 0, + newLup: 3_844.432207828138682757 * 1e18 + }); // assert balances assertEq(_quote.balanceOf(address(_pool)), 6_000 * 1e18); @@ -462,19 +419,17 @@ contract ERC20PoolLiquidationsKickWithDepositTest is ERC20HelperContract { assertEq(_quote.balanceOf(_borrower5), 20_000 * 1e18); // lender 2 kicks using all LPs from bucket 2499 (10_000) and sending additional quote tokens to cover auction bond (510.096153846153851000) - _kickWithDeposit( - { - from: _lender2, - index: 2499, - borrower: _borrower1, - debt: 35_471.574519230769247125 * 1e18, - collateral: 1_000 * 1e18, - bond: 10_510.096153846153851000 * 1e18, - removedFromDeposit: 10_000 * 1e18, - transferAmount: 510.096153846153851000 * 1e18, - lup: 99836282890 - } - ); + _kickWithDeposit({ + from: _lender2, + index: 2499, + borrower: _borrower1, + debt: 35_471.574519230769247125 * 1e18, + collateral: 1_000 * 1e18, + bond: 10_510.096153846153851000 * 1e18, + removedFromDeposit: 10_000 * 1e18, + transferAmount: 510.096153846153851000 * 1e18, + lup: 99836282890 + }); /******************************/ /*** Assert post-kick state ***/ @@ -497,6 +452,7 @@ contract ERC20PoolLiquidationsKickWithDepositTest is ERC20HelperContract { interestRateUpdate: _startTime }) ); + // assert balances assertEq(_quote.balanceOf(address(_pool)), 6_510.096153846153851000 * 1e18); // increased with the amount sent to cover bond assertEq(_quote.balanceOf(_lender1), 49_000 * 1e18); @@ -507,33 +463,28 @@ contract ERC20PoolLiquidationsKickWithDepositTest is ERC20HelperContract { assertEq(_quote.balanceOf(_borrower3), 20_000 * 1e18); assertEq(_quote.balanceOf(_borrower4), 20_000 * 1e18); assertEq(_quote.balanceOf(_borrower5), 20_000 * 1e18); + // assert lenders LPs in bucket used - _assertLenderLpBalance( - { - lender: _lender2, - index: 2499, - lpBalance: 0, - depositTime: _startTime - } - ); + _assertLenderLpBalance({ + lender: _lender2, + index: 2499, + lpBalance: 0, + depositTime: _startTime + }); // assert bucket - LPs and deposit obliterated - _assertBucket( - { - index: 2499, - lpBalance: 0, - collateral: 0, - deposit: 0, - exchangeRate: 1 * 1e27 - } - ); + _assertBucket({ + index: 2499, + lpBalance: 0, + collateral: 0, + deposit: 0, + exchangeRate: 1 * 1e18 + }); // assert lender2 as a kicker - _assertKicker( - { - kicker: _lender2, - claimable: 0, - locked: 10_510.096153846153851000 * 1e18 - } - ); + _assertKicker({ + kicker: _lender2, + claimable: 0, + locked: 10_510.096153846153851000 * 1e18 + }); // assert kicked auction _assertAuction( AuctionParams({ @@ -548,7 +499,7 @@ contract ERC20PoolLiquidationsKickWithDepositTest is ERC20HelperContract { auctionPrice: 123_636.939803752939029248 * 1e18, debtInAuction: 35_471.574519230769247125 * 1e18, thresholdPrice: 35.471574519230769247 * 1e18, - neutralPrice: 37.154109537259614798 * 1e18 + neutralPrice: 36.969263221153845869 * 1e18 }) ); } @@ -561,46 +512,39 @@ contract ERC20PoolLiquidationsKickWithDepositTest is ERC20HelperContract { */ // lender1 adds collateral to bucket to be entitled to higher deposit than available - _addCollateral( - { - from: _lender1, - amount: 10 * 1e18, - index: 2500, - lpAward: 38636.54368867279344664 * 1e27 - } - ); + _addCollateral({ + from: _lender1, + amount: 10 * 1e18, + index: 2500, + lpAward: 38636.54368867279344664 * 1e18 + }); + // assert lender and bucket LP balances pre kick - _assertLenderLpBalance( - { - lender: _lender1, - index: 2500, - lpBalance: 88_636.54368867279344664 * 1e27, - depositTime: _startTime - } - ); - _assertBucket( - { - index: 2500, - lpBalance: 98_636.54368867279344664 * 1e27, - collateral: 10 * 1e18, - deposit: 60_000 * 1e18, - exchangeRate: 1 * 1e27 - } - ); + _assertLenderLpBalance({ + lender: _lender1, + index: 2500, + lpBalance: 88_636.54368867279344664 * 1e18, + depositTime: _startTime + }); + _assertBucket({ + index: 2500, + lpBalance: 98_636.54368867279344664 * 1e18, + collateral: 10 * 1e18, + deposit: 60_000 * 1e18, + exchangeRate: 1 * 1e18 + }); - _kickWithDeposit( - { - from: _lender1, - index: 2500, - borrower: _borrower1, - debt: 20_269.471153846153855500 * 1e18, - collateral: 1_000 * 1e18, - bond: 6_005.769230769230772000 * 1e18, - removedFromDeposit: 6_005.769230769230772000 * 1e18, - transferAmount: 0, - lup: 3_844.432207828138682757 * 1e18 - } - ); + _kickWithDeposit({ + from: _lender1, + index: 2500, + borrower: _borrower1, + debt: 20_269.471153846153855500 * 1e18, + collateral: 1_000 * 1e18, + bond: 6_005.769230769230772000 * 1e18, + removedFromDeposit: 6_005.769230769230772000 * 1e18, + transferAmount: 0, + lup: 3_844.432207828138682757 * 1e18 + }); /******************************/ /*** Assert post-kick state ***/ @@ -623,6 +567,7 @@ contract ERC20PoolLiquidationsKickWithDepositTest is ERC20HelperContract { interestRateUpdate: _startTime }) ); + // assert balances - no change, bond was covered from deposit assertEq(_quote.balanceOf(address(_pool)), 11_000 * 1e18); assertEq(_quote.balanceOf(_lender1), 49_000 * 1e18); @@ -632,41 +577,34 @@ contract ERC20PoolLiquidationsKickWithDepositTest is ERC20HelperContract { assertEq(_quote.balanceOf(_borrower3), 20_000 * 1e18); assertEq(_quote.balanceOf(_borrower4), 20_000 * 1e18); assertEq(_quote.balanceOf(_borrower5), 20_000 * 1e18); + // assert lenders LPs in bucket used - _assertLenderLpBalance( - { - lender: _lender1, - index: 2500, - lpBalance: 82_630.77445790356267464 * 1e27, // reduced by amount used to cover auction bond - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: _lender2, - index: 2500, - lpBalance: 10_000 * 1e27, - depositTime: _startTime - } - ); + _assertLenderLpBalance({ + lender: _lender1, + index: 2500, + lpBalance: 82_630.77445790356267464 * 1e18, // reduced by amount used to cover auction bond + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: _lender2, + index: 2500, + lpBalance: 10_000 * 1e18, + depositTime: _startTime + }); // assert bucket LPs - _assertBucket( - { - index: 2500, - lpBalance: 92_630.77445790356267464 * 1e27, // reduced by amount used to cover auction bond - collateral: 10 * 1e18, - deposit: 53_994.230769230769228000 * 1e18, // reduced by amount used to cover auction bond - exchangeRate: 1 * 1e27 - } - ); + _assertBucket({ + index: 2500, + lpBalance: 92_630.77445790356267464 * 1e18, // reduced by amount used to cover auction bond + collateral: 10 * 1e18, + deposit: 53_994.230769230769228000 * 1e18, // reduced by amount used to cover auction bond + exchangeRate: 1 * 1e18 + }); // assert lender1 as a kicker - _assertKicker( - { - kicker: _lender1, - claimable: 0, - locked: 6_005.769230769230772000 * 1e18 - } - ); + _assertKicker({ + kicker: _lender1, + claimable: 0, + locked: 6_005.769230769230772000 * 1e18 + }); // assert kicked auction _assertAuction( AuctionParams({ @@ -708,19 +646,18 @@ contract ERC20PoolLiquidationsKickWithDepositTest is ERC20HelperContract { assertEq(thresholdPrice, 20.019230769230769240 * 1e18); // kick borrower 1 - _kickWithDeposit( - { - from: _lender1, - index: 2500, - borrower: _borrower1, - debt: 20_269.471153846153855500 * 1e18, - collateral: 1_000 * 1e18, - bond: 6_005.769230769230772000 * 1e18, - removedFromDeposit: 6_005.769230769230772000 * 1e18, - transferAmount: 0, - lup: 3_844.432207828138682757 * 1e18 - } - ); + _kickWithDeposit({ + from: _lender1, + index: 2500, + borrower: _borrower1, + debt: 20_269.471153846153855500 * 1e18, + collateral: 1_000 * 1e18, + bond: 6_005.769230769230772000 * 1e18, + removedFromDeposit: 6_005.769230769230772000 * 1e18, + transferAmount: 0, + lup: 3_844.432207828138682757 * 1e18 + }); + (borrower, thresholdPrice) = _pool.loanInfo(1); assertEq(borrower, _borrower5); assertEq(thresholdPrice, 20.019230769230769240 * 1e18); @@ -748,19 +685,18 @@ contract ERC20PoolLiquidationsKickWithDepositTest is ERC20HelperContract { assertEq(prev, address(0)); // kick borrower 5 - _kickWithDeposit( - { - from: _lender1, - index: 2500, - borrower: _borrower5, - debt: 20_269.471153846153855500 * 1e18, - collateral: 1_000 * 1e18, - bond: 6_005.769230769230772000 * 1e18, - removedFromDeposit: 6_005.769230769230772000 * 1e18, - transferAmount: 0, - lup: 99836282890 - } - ); + _kickWithDeposit({ + from: _lender1, + index: 2500, + borrower: _borrower5, + debt: 20_269.471153846153855500 * 1e18, + collateral: 1_000 * 1e18, + bond: 6_005.769230769230772000 * 1e18, + removedFromDeposit: 6_005.769230769230772000 * 1e18, + transferAmount: 0, + lup: 99836282890 + }); + (borrower, thresholdPrice) = _pool.loanInfo(1); assertEq(borrower, _borrower4); assertEq(thresholdPrice, 20.019230769230769240 * 1e18); @@ -787,19 +723,18 @@ contract ERC20PoolLiquidationsKickWithDepositTest is ERC20HelperContract { assertEq(prev, _borrower1); // kick borrower 4 - _kickWithDeposit( - { - from: _lender1, - index: 2500, - borrower: _borrower4, - debt: 20_269.471153846153855500 * 1e18, - collateral: 1_000 * 1e18, - bond: 6_005.769230769230772000 * 1e18, - removedFromDeposit: 6_005.769230769230772000 * 1e18, - transferAmount: 0, - lup: 99836282890 - } - ); + _kickWithDeposit({ + from: _lender1, + index: 2500, + borrower: _borrower4, + debt: 20_269.471153846153855500 * 1e18, + collateral: 1_000 * 1e18, + bond: 6_005.769230769230772000 * 1e18, + removedFromDeposit: 6_005.769230769230772000 * 1e18, + transferAmount: 0, + lup: 99836282890 + }); + (borrower, thresholdPrice) = _pool.loanInfo(1); assertEq(borrower, _borrower3); assertEq(thresholdPrice, 20.019230769230769240 * 1e18); @@ -830,19 +765,18 @@ contract ERC20PoolLiquidationsKickWithDepositTest is ERC20HelperContract { assertEq(prev, _borrower5); // kick borrower 3 - _kickWithDeposit( - { - from: _lender1, - index: 2500, - borrower: _borrower3, - debt: 20_269.471153846153855500 * 1e18, - collateral: 1_000 * 1e18, - bond: 6_005.769230769230772000 * 1e18, - removedFromDeposit: 6_005.769230769230772000 * 1e18, - transferAmount: 0, - lup: 99836282890 - } - ); + _kickWithDeposit({ + from: _lender1, + index: 2500, + borrower: _borrower3, + debt: 20_269.471153846153855500 * 1e18, + collateral: 1_000 * 1e18, + bond: 6_005.769230769230772000 * 1e18, + removedFromDeposit: 6_005.769230769230772000 * 1e18, + transferAmount: 0, + lup: 99836282890 + }); + (borrower, thresholdPrice) = _pool.loanInfo(1); assertEq(borrower, _borrower2); assertEq(thresholdPrice, 20.019230769230769240 * 1e18); @@ -877,19 +811,18 @@ contract ERC20PoolLiquidationsKickWithDepositTest is ERC20HelperContract { assertEq(prev, _borrower4); // kick borrower 2 - _kickWithDeposit( - { - from: _lender1, - index: 2500, - borrower: _borrower2, - debt: 20_269.471153846153855500 * 1e18, - collateral: 1_000 * 1e18, - bond: 6_005.769230769230772000 * 1e18, - removedFromDeposit: 6_005.769230769230772000 * 1e18, - transferAmount: 0, - lup: 99836282890 - } - ); + _kickWithDeposit({ + from: _lender1, + index: 2500, + borrower: _borrower2, + debt: 20_269.471153846153855500 * 1e18, + collateral: 1_000 * 1e18, + bond: 6_005.769230769230772000 * 1e18, + removedFromDeposit: 6_005.769230769230772000 * 1e18, + transferAmount: 0, + lup: 99836282890 + }); + (borrower, thresholdPrice) = _pool.loanInfo(1); assertEq(borrower, address(0)); assertEq(thresholdPrice, 0); @@ -948,6 +881,7 @@ contract ERC20PoolLiquidationsKickWithDepositTest is ERC20HelperContract { // skip to make loans clearable skip(80 hours); + // settle borrower 2 _assertAuction( AuctionParams({ @@ -965,14 +899,14 @@ contract ERC20PoolLiquidationsKickWithDepositTest is ERC20HelperContract { neutralPrice: 21.020192307692307702 * 1e18 }) ); - _settle( - { - from: _lender1, - borrower: _borrower2, - maxDepth: 1, - settledDebt: 20_269.471153846153855500 * 1e18 - } - ); + + _settle({ + from: _lender1, + borrower: _borrower2, + maxDepth: 1, + settledDebt: 20_269.471153846153855500 * 1e18 + }); + _assertAuction( AuctionParams({ borrower: _borrower2, @@ -989,6 +923,7 @@ contract ERC20PoolLiquidationsKickWithDepositTest is ERC20HelperContract { neutralPrice: 0 }) ); + (, , , , , , head, next, prev) = _pool.auctionInfo(_borrower1); assertEq(head, _borrower1); assertEq(next, _borrower5); @@ -1027,14 +962,14 @@ contract ERC20PoolLiquidationsKickWithDepositTest is ERC20HelperContract { neutralPrice: 21.125293269230769068 * 1e18 }) ); - _settle( - { - from: _lender1, - borrower: _borrower4, - maxDepth: 5, - settledDebt: 20_269.471153846153855500 * 1e18 - } - ); + + _settle({ + from: _lender1, + borrower: _borrower4, + maxDepth: 5, + settledDebt: 20_269.471153846153855500 * 1e18 + }); + _assertAuction( AuctionParams({ borrower: _borrower4, @@ -1051,6 +986,7 @@ contract ERC20PoolLiquidationsKickWithDepositTest is ERC20HelperContract { neutralPrice: 0 }) ); + (, , , , , , head, next, prev) = _pool.auctionInfo(_borrower1); assertEq(head, _borrower1); assertEq(next, _borrower5); @@ -1089,14 +1025,14 @@ contract ERC20PoolLiquidationsKickWithDepositTest is ERC20HelperContract { neutralPrice: 21.020192307692307702 * 1e18 }) ); - _settle( - { - from: _lender1, - borrower: _borrower1, - maxDepth: 5, - settledDebt: 20_269.471153846153855500 * 1e18 - } - ); + + _settle({ + from: _lender1, + borrower: _borrower1, + maxDepth: 5, + settledDebt: 20_269.471153846153855500 * 1e18 + }); + _assertAuction( AuctionParams({ borrower: _borrower1, @@ -1113,6 +1049,7 @@ contract ERC20PoolLiquidationsKickWithDepositTest is ERC20HelperContract { neutralPrice: 0 }) ); + (, , , , , , head, next, prev) = _pool.auctionInfo(_borrower1); assertEq(head, _borrower5); assertEq(next, address(0)); @@ -1151,14 +1088,14 @@ contract ERC20PoolLiquidationsKickWithDepositTest is ERC20HelperContract { neutralPrice: 21.230919735576922742 * 1e18 }) ); - _settle( - { - from: _lender1, - borrower: _borrower5, - maxDepth: 5, - settledDebt: 20_269.471153846153855500 * 1e18 - } - ); + + _settle({ + from: _lender1, + borrower: _borrower5, + maxDepth: 5, + settledDebt: 20_269.471153846153855500 * 1e18 + }); + _assertAuction( AuctionParams({ borrower: _borrower5, @@ -1175,6 +1112,7 @@ contract ERC20PoolLiquidationsKickWithDepositTest is ERC20HelperContract { neutralPrice: 0 }) ); + (, , , , , , head, next, prev) = _pool.auctionInfo(_borrower1); assertEq(head, _borrower3); assertEq(next, address(0)); @@ -1213,14 +1151,14 @@ contract ERC20PoolLiquidationsKickWithDepositTest is ERC20HelperContract { neutralPrice: 21.125293269230769068 * 1e18 }) ); - _settle( - { - from: _lender1, - borrower: _borrower3, - maxDepth: 5, - settledDebt: 20_269.471153846153855500 * 1e18 - } - ); + + _settle({ + from: _lender1, + borrower: _borrower3, + maxDepth: 5, + settledDebt: 20_269.471153846153855500 * 1e18 + }); + _assertAuction( AuctionParams({ borrower: _borrower3, @@ -1237,6 +1175,7 @@ contract ERC20PoolLiquidationsKickWithDepositTest is ERC20HelperContract { neutralPrice: 0 }) ); + (, , , , , , head, next, prev) = _pool.auctionInfo(_borrower1); assertEq(head, address(0)); assertEq(next, address(0)); @@ -1277,124 +1216,102 @@ contract ERC20PoolLiquidationsKickWithDepositTest is ERC20HelperContract { }) ); // assert lender1 after settle - _assertLenderLpBalance( - { - lender: _lender1, - index: 2500, - lpBalance: 19_971.15384615384614 * 1e27, - depositTime: _startTime - } - ); + _assertLenderLpBalance({ + lender: _lender1, + index: 2500, + lpBalance: 19_971.15384615384614 * 1e18, + depositTime: _startTime + }); // assert lender1 as a kicker - _assertKicker( - { - kicker: _lender1, - claimable: 30_028.846153846153860000 * 1e18, - locked: 0 - } - ); + _assertKicker({ + kicker: _lender1, + claimable: 30_028.846153846153860000 * 1e18, + locked: 0 + }); // assert borrowers after settle - _assertBorrower( - { - borrower: _borrower1, - borrowerDebt: 0, - borrowerCollateral: 994.725169378126588636 * 1e18, - borrowert0Np: 21.020192307692307702 * 1e18, - borrowerCollateralization: 1 * 1e18 - } - ); - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 0, - borrowerCollateral: 994.751412316543869246 * 1e18, - borrowert0Np: 21.020192307692307702 * 1e18, - borrowerCollateralization: 1 * 1e18 - } - ); - _assertBorrower( - { - borrower: _borrower3, - borrowerDebt: 0, - borrowerCollateral: 0, // last borrower settled - borrowert0Np: 21.125293269230769068 * 1e18, - borrowerCollateralization: 1 * 1e18 - } - ); - _assertBorrower( - { - borrower: _borrower4, - borrowerDebt: 0, - borrowerCollateral: 994.737734630435747061 * 1e18, - borrowert0Np: 21.125293269230769068 * 1e18, - borrowerCollateralization: 1 * 1e18 - } - ); - _assertBorrower( - { - borrower: _borrower5, - borrowerDebt: 0, - borrowerCollateral: 0, - borrowert0Np: 21.230919735576922742 * 1e18, - borrowerCollateralization: 1 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower1, + borrowerDebt: 0, + borrowerCollateral: 994.725169378126588636 * 1e18, + borrowert0Np: 21.020192307692307702 * 1e18, + borrowerCollateralization: 1 * 1e18 + }); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 0, + borrowerCollateral: 994.751412316543869246 * 1e18, + borrowert0Np: 21.020192307692307702 * 1e18, + borrowerCollateralization: 1 * 1e18 + }); + _assertBorrower({ + borrower: _borrower3, + borrowerDebt: 0, + borrowerCollateral: 0, // last borrower settled + borrowert0Np: 21.125293269230769068 * 1e18, + borrowerCollateralization: 1 * 1e18 + }); + _assertBorrower({ + borrower: _borrower4, + borrowerDebt: 0, + borrowerCollateral: 994.737734630435747061 * 1e18, + borrowert0Np: 21.125293269230769068 * 1e18, + borrowerCollateralization: 1 * 1e18 + }); + _assertBorrower({ + borrower: _borrower5, + borrowerDebt: 0, + borrowerCollateral: 0, + borrowert0Np: 21.230919735576922742 * 1e18, + borrowerCollateralization: 1 * 1e18 + }); } function testKickWithDepositReverts() external tearDown { // assert lender cannot kick with a bucket without deposit - _assertKickWithInsufficientLiquidityRevert( - { - from: _lender1, - index: 2_503 - } - ); + _assertKickWithInsufficientLiquidityRevert({ + from: _lender1, + index: 2_503 + }); + // assert lender cannot kick a loan if the proposed LUP doesn't render borrower uncollateralized - _assertKickWithBadProposedLupRevert( - { - from: _lender2, - index: 2_500 - } - ); + _assertKickWithBadProposedLupRevert({ + from: _lender2, + index: 2_500 + }); // Lender 2 adds Quote token at a much lower price the tries to kick with deposit - _addLiquidity( - { - from: _lender3, - amount: 150_000 * 1e18, - index: 7000, - lpAward: 150_000 * 1e27, - newLup: 3_844.432207828138682757 * 1e18 - } - ); - _assertKickPriceBelowProposedLupRevert( - { - from: _lender3, - index: 7000 - } - ); + _addLiquidity({ + from: _lender3, + amount: 150_000 * 1e18, + index: 7000, + lpAward: 150_000 * 1e18, + newLup: 3_844.432207828138682757 * 1e18 + }); + + _assertKickPriceBelowProposedLupRevert({ + from: _lender3, + index: 7000 + }); // asert failure when lender has LPs but insufficient quote token balance to post remaining bond - _addLiquidity( - { - from: _lender4, - amount: 5_000 * 1e18, - index: 2499, - lpAward: 5_000 * 1e27, - newLup: 3_844.432207828138682757 * 1e18 - } - ); + _addLiquidity({ + from: _lender4, + amount: 5_000 * 1e18, + index: 2499, + lpAward: 5_000 * 1e18, + newLup: 3_844.432207828138682757 * 1e18 + }); + // borrower draws more debt consuming entire deposit from bucket 2499 - _drawDebt( - { - from: _borrower1, - borrower: _borrower1, - amountToBorrow: 15_000 * 1e18, - limitIndex: 5000, - collateralToPledge: 0, - newLup: 3_825.305679430983794766 * 1e18 - } - ); + _drawDebt({ + from: _borrower1, + borrower: _borrower1, + amountToBorrow: 15_000 * 1e18, + limitIndex: 5000, + collateralToPledge: 0, + newLup: 3_825.305679430983794766 * 1e18 + }); + changePrank(_lender4); vm.expectRevert("ERC20: transfer amount exceeds balance"); _pool.kickWithDeposit(2499); diff --git a/tests/forge/ERC20Pool/ERC20PoolLiquidationsMisc.t.sol b/tests/forge/ERC20Pool/ERC20PoolLiquidationsMisc.t.sol index 540295041..f01816df5 100644 --- a/tests/forge/ERC20Pool/ERC20PoolLiquidationsMisc.t.sol +++ b/tests/forge/ERC20Pool/ERC20PoolLiquidationsMisc.t.sol @@ -27,75 +27,57 @@ contract ERC20PoolLiquidationsMiscTest is ERC20HelperContract { _mintCollateralAndApproveTokens(_lender1, 4 * 1e18); // Lender adds Quote token accross 5 prices - _addInitialLiquidity( - { - from: _lender, - amount: 2_000 * 1e18, - index: _i9_91 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 5_000 * 1e18, - index: _i9_81 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 11_000 * 1e18, - index: _i9_72 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 25_000 * 1e18, - index: _i9_62 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 30_000 * 1e18, - index: _i9_52 - } - ); + _addInitialLiquidity({ + from: _lender, + amount: 2_000 * 1e18, + index: _i9_91 + }); + _addInitialLiquidity({ + from: _lender, + amount: 5_000 * 1e18, + index: _i9_81 + }); + _addInitialLiquidity({ + from: _lender, + amount: 11_000 * 1e18, + index: _i9_72 + }); + _addInitialLiquidity({ + from: _lender, + amount: 25_000 * 1e18, + index: _i9_62 + }); + _addInitialLiquidity({ + from: _lender, + amount: 30_000 * 1e18, + index: _i9_52 + }); // first borrower adds collateral token and borrows - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - amount: 2 * 1e18 - } - ); - _borrow( - { - from: _borrower, - amount: 19.25 * 1e18, - indexLimit: _i9_91, - newLup: 9.917184843435912074 * 1e18 - } - ); + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + amount: 2 * 1e18 + }); + _borrow({ + from: _borrower, + amount: 19.25 * 1e18, + indexLimit: _i9_91, + newLup: 9.917184843435912074 * 1e18 + }); // second borrower adds collateral token and borrows - _pledgeCollateral( - { - from: _borrower2, - borrower: _borrower2, - amount: 1_000 * 1e18 - } - ); - _borrow( - { - from: _borrower2, - amount: 7_980 * 1e18, - indexLimit: _i9_72, - newLup: 9.721295865031779605 * 1e18 - } - ); + _pledgeCollateral({ + from: _borrower2, + borrower: _borrower2, + amount: 1_000 * 1e18 + }); + _borrow({ + from: _borrower2, + amount: 7_980 * 1e18, + indexLimit: _i9_72, + newLup: 9.721295865031779605 * 1e18 + }); /*****************************/ /*** Assert pre-kick state ***/ @@ -118,43 +100,36 @@ contract ERC20PoolLiquidationsMiscTest is ERC20HelperContract { interestRateUpdate: _startTime }) ); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 19.268509615384615394 * 1e18, - borrowerCollateral: 2 * 1e18, - borrowert0Np: 10.115967548076923081 * 1e18, - borrowerCollateralization: 1.009034539679184679 * 1e18 - } - ); - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 7_987.673076923076926760 * 1e18, - borrowerCollateral: 1_000 * 1e18, - borrowert0Np: 8.471136974495192174 * 1e18, - borrowerCollateralization: 1.217037273735858713 * 1e18 - } - ); - _assertReserveAuction( - { - reserves: 7.691586538461542154 * 1e18, - claimableReserves : 0, - claimableReservesRemaining: 0, - auctionPrice: 0, - timeRemaining: 0 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 19.268509615384615394 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 10.115967548076923081 * 1e18, + borrowerCollateralization: 1.009034539679184679 * 1e18 + }); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 7_987.673076923076926760 * 1e18, + borrowerCollateral: 1_000 * 1e18, + borrowert0Np: 8.471136974495192174 * 1e18, + borrowerCollateralization: 1.217037273735858713 * 1e18 + }); + _assertReserveAuction({ + reserves: 7.691586538461542154 * 1e18, + claimableReserves : 0, + claimableReservesRemaining: 0, + auctionPrice: 0, + timeRemaining: 0 + }); + assertEq(_quote.balanceOf(_lender), 47_000 * 1e18); // should revert if there's no auction started - _assertTakeNoAuctionRevert( - { - from: _lender, - borrower: _borrower, - maxCollateral: 10 * 1e18 - } - ); + _assertTakeNoAuctionRevert({ + from: _lender, + borrower: _borrower, + maxCollateral: 10 * 1e18 + }); } @@ -163,53 +138,41 @@ contract ERC20PoolLiquidationsMiscTest is ERC20HelperContract { skip(25 hours); // Lender attempts to withdraw entire position - _removeLiquidity( - { - from: _lender, - amount: 2_000.00 * 1e18, - index: _i9_91, - newLup: 9.721295865031779605 * 1e18, - lpRedeem: 1_999.891367962935869240000000000 * 1e27 - } - ); - - _removeLiquidity( - { - from: _lender, - amount: 5_000 * 1e18, - index: _i9_81, - newLup: 9.721295865031779605 * 1e18, - lpRedeem: 4_999.728419907339673101000000000 * 1e27 - } - ); - - _removeLiquidity( - { - from: _lender, - amount: 2_992.8 * 1e18, - index: _i9_72, - newLup: 9.721295865031779605 * 1e18, - lpRedeem: 2_992.637443019737234731000000000 * 1e27 - } - ); + _removeLiquidity({ + from: _lender, + amount: 2_000.00 * 1e18, + index: _i9_91, + newLup: 9.721295865031779605 * 1e18, + lpRedeem: 1_999.891367962935869240 * 1e18 + }); + _removeLiquidity({ + from: _lender, + amount: 5_000 * 1e18, + index: _i9_81, + newLup: 9.721295865031779605 * 1e18, + lpRedeem: 4_999.728419907339673101 * 1e18 + }); + _removeLiquidity({ + from: _lender, + amount: 2_992.8 * 1e18, + index: _i9_72, + newLup: 9.721295865031779605 * 1e18, + lpRedeem: 2_992.637443019737234731 * 1e18 + }); // Lender amount to withdraw is restricted by HTP - _assertRemoveAllLiquidityLupBelowHtpRevert( - { - from: _lender, - index: _i9_72 - } - ); + _assertRemoveAllLiquidityLupBelowHtpRevert({ + from: _lender, + index: _i9_72 + }); - _assertBucket( - { - index: _i9_72, - lpBalance: 8_007.362556980262765269000000000 * 1e27, - collateral: 0, - deposit: 8_007.797508658144068000 * 1e18, - exchangeRate: 1.000054318968922187999940710 * 1e27 - } - ); + _assertBucket({ + index: _i9_72, + lpBalance: 8_007.362556980262765269 * 1e18, + collateral: 0, + deposit: 8_007.797508658144068000 * 1e18, + exchangeRate: 1.000054318968922188 * 1e18 + }); skip(16 hours); @@ -229,38 +192,30 @@ contract ERC20PoolLiquidationsMiscTest is ERC20HelperContract { neutralPrice: 0 }) ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 19.272843317722413898 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 10.115967548076923081 * 1e18, + borrowerCollateralization: 0.998794730435100101 * 1e18 + }); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 19.272843317722413898 * 1e18, - borrowerCollateral: 2 * 1e18, - borrowert0Np: 10.115967548076923081 * 1e18, - borrowerCollateralization: 0.998794730435100101 * 1e18 - } - ); - - _kick( - { - from: _lender, - borrower: _borrower, - debt: 19.489662805046791054 * 1e18, - collateral: 2 * 1e18, - bond: 0.192728433177224139 * 1e18, - transferAmount: 0.192728433177224139 * 1e18 - } - ); - - _assertBucket( - { - index: _i9_72, - lpBalance: 8_007.362556980262765269000000000 * 1e27, - collateral: 0, - deposit: 8_008.361347558277120605 * 1e18, - exchangeRate: 1.000124734027079076000026734 * 1e27 - } - ); + _kick({ + from: _lender, + borrower: _borrower, + debt: 19.489662805046791054 * 1e18, + collateral: 2 * 1e18, + bond: 0.192728433177224139 * 1e18, + transferAmount: 0.192728433177224139 * 1e18 + }); + _assertBucket({ + index: _i9_72, + lpBalance: 8_007.362556980262765269 * 1e18, + collateral: 0, + deposit: 8_008.361347558277120605 * 1e18, + exchangeRate: 1.000124734027079076 * 1e18 + }); _assertAuction( AuctionParams({ borrower: _borrower, @@ -279,41 +234,27 @@ contract ERC20PoolLiquidationsMiscTest is ERC20HelperContract { ); // lender cannot withdraw - deposit in buckets within liquidation debt from the top-of-book down are frozen - _assertRemoveDepositLockedByAuctionDebtRevert( - { - from: _lender, - amount: 10.0 * 1e18, - index: _i9_72 - } - ); + _assertRemoveDepositLockedByAuctionDebtRevert({ + from: _lender, + amount: 10.0 * 1e18, + index: _i9_72 + }); // lender cannot move funds - _assertMoveDepositLockedByAuctionDebtRevert( - { - from: _lender, - amount: 10.0 * 1e18, - fromIndex: _i9_72, - toIndex: _i9_81 - } - ); + _assertMoveDepositLockedByAuctionDebtRevert({ + from: _lender, + amount: 10.0 * 1e18, + fromIndex: _i9_72, + toIndex: _i9_81 + }); // lender can add / remove liquidity in buckets that are not within liquidation debt changePrank(_lender1); - _pool.addQuoteToken(2_000 * 1e18, 5000); + _pool.addQuoteToken(2_000 * 1e18, 5000, block.timestamp + 1 minutes); _pool.removeQuoteToken(2_000 * 1e18, 5000); skip(3 hours); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 19.489933125874732298 * 1e18, - borrowerCollateral: 2 * 1e18, - borrowert0Np: 10.115967548076923081 * 1e18, - borrowerCollateralization: 0.987669594447545452 * 1e18 - } - ); - _assertAuction( AuctionParams({ borrower: _borrower, @@ -330,18 +271,23 @@ contract ERC20PoolLiquidationsMiscTest is ERC20HelperContract { neutralPrice: 10.118242741804267296 * 1e18 }) ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 19.489933125874732298 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 10.115967548076923081 * 1e18, + borrowerCollateralization: 0.987669594447545452 * 1e18 + }); - _take( - { - from: _lender, - borrower: _borrower, - maxCollateral: 2.0 * 1e18, - bondChange: 0.192728433177224139 * 1e18, - givenAmount: 20.854228444685963559 * 1e18, - collateralTaken: 0.257631549479994909 * 1e18, - isReward: false - } - ); + _take({ + from: _lender, + borrower: _borrower, + maxCollateral: 2.0 * 1e18, + bondChange: 0.192728433177224139 * 1e18, + givenAmount: 20.854228444685963559 * 1e18, + collateralTaken: 0.257631549479994909 * 1e18, + isReward: false + }); // Borrower is removed from auction, keeps collateral in system _assertAuction( @@ -360,17 +306,13 @@ contract ERC20PoolLiquidationsMiscTest is ERC20HelperContract { neutralPrice: 0 }) ); - - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 0, - borrowerCollateral: 1.742368450520005091 * 1e18, - borrowert0Np: 0, - borrowerCollateralization: 1 * 1e18 - } - ); - + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 0, + borrowerCollateral: 1.742368450520005091 * 1e18, + borrowert0Np: 0, + borrowerCollateralization: 1 * 1e18 + }); _assertPool( PoolParams({ htp: 7.991488192808991114 * 1e18, @@ -389,65 +331,53 @@ contract ERC20PoolLiquidationsMiscTest is ERC20HelperContract { }) ); - _removeLiquidity( - { - from: _lender, - amount: 8_008.373442262808822463 * 1e18, - index: _i9_72, - newLup: 9.624807173121239337 * 1e18, - lpRedeem: 8_007.362556980262762824000000000 * 1e27 - } - ); + _removeLiquidity({ + from: _lender, + amount: 8_008.373442262808822463 * 1e18, + index: _i9_72, + newLup: 9.624807173121239337 * 1e18, + lpRedeem: 8_007.362556980262762824 * 1e18 + }); - _assertBucket( - { - index: _i9_72, - lpBalance: 0.000000000000002445000000000 * 1e27, - collateral: 0, - deposit: 2445, - exchangeRate: 1 * 1e27 - } - ); + _assertBucket({ + index: _i9_72, + lpBalance: 0.000000000000002445 * 1e18, + collateral: 0, + deposit: 2445, + exchangeRate: 1 * 1e18 + }); - _removeLiquidity( - { - from: _lender, - amount: 25_000.037756489769875000 * 1e18, - index: _i9_62, - newLup: 9.529276179422528643 * 1e18, - lpRedeem: 25_000 * 1e27 - } - ); + _removeLiquidity({ + from: _lender, + amount: 25_000.037756489769875000 * 1e18, + index: _i9_62, + newLup: 9.529276179422528643 * 1e18, + lpRedeem: 25_000 * 1e18 + }); - _assertBucket( - { - index: _i9_62, - lpBalance: 0, - collateral: 0, - deposit: 0, - exchangeRate: 1 * 1e27 - } - ); + _assertBucket({ + index: _i9_62, + lpBalance: 0, + collateral: 0, + deposit: 0, + exchangeRate: 1 * 1e18 + }); - _removeLiquidity( - { - from: _lender, - amount: 22_000 * 1e18, - index: _i9_52, - newLup: 9.529276179422528643 * 1e18, - lpRedeem: 21_999.966774339181882911000000000 * 1e27 - } - ); + _removeLiquidity({ + from: _lender, + amount: 22_000 * 1e18, + index: _i9_52, + newLup: 9.529276179422528643 * 1e18, + lpRedeem: 21_999.966774339181882911 * 1e18 + }); - _assertBucket( - { - index: _i9_52, - lpBalance: 8_000.033225660818117089000000000 * 1e27, - collateral: 0, - deposit: 8_000.045307787723850000 * 1e18, - exchangeRate: 1.000001510259590794999992127 * 1e27 - } - ); + _assertBucket({ + index: _i9_52, + lpBalance: 8_000.033225660818117089 * 1e18, + collateral: 0, + deposit: 8_000.045307787723850000 * 1e18, + exchangeRate: 1.000001510259590795 * 1e18 + }); _assertRemoveAllLiquidityLupBelowHtpRevert({ from: _lender, @@ -473,16 +403,13 @@ contract ERC20PoolLiquidationsMiscTest is ERC20HelperContract { interestRateUpdate: block.timestamp - 28 hours }) ); - - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 7_990.503913730158190391 * 1e18, - borrowerCollateral: 1_000.00 * 1e18, - borrowert0Np: 8.471136974495192174 * 1e18, - borrowerCollateralization: 1.192575121957988603 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 7_990.503913730158190391 * 1e18, + borrowerCollateral: 1_000.00 * 1e18, + borrowert0Np: 8.471136974495192174 * 1e18, + borrowerCollateralization: 1.192575121957988603 * 1e18 + }); // draw debt is called to trigger accrual of pool interest that will push the lup back up IERC20Pool(address(_pool)).drawDebt(_lender, 0, 0, 0); @@ -507,27 +434,23 @@ contract ERC20PoolLiquidationsMiscTest is ERC20HelperContract { skip(117 days); - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 8_105.430237884635118282 * 1e18, - borrowerCollateral: 1_000 * 1e18, - borrowert0Np: 8.471136974495192174 * 1e18, - borrowerCollateralization: 0.000000012317209569 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 8_105.430237884635118282 * 1e18, + borrowerCollateral: 1_000 * 1e18, + borrowert0Np: 8.471136974495192174 * 1e18, + borrowerCollateralization: 0.000000012317209569 * 1e18 + }); // kick borrower 2 changePrank(_lender); _pool.kickWithDeposit(_i9_52); - _assertRemoveDepositLockedByAuctionDebtRevert( - { - from: _lender, - amount: 10.0 * 1e18, - index: _i9_52 - } - ); + _assertRemoveDepositLockedByAuctionDebtRevert({ + from: _lender, + amount: 10.0 * 1e18, + index: _i9_52 + }); skip(10 hours); @@ -547,28 +470,23 @@ contract ERC20PoolLiquidationsMiscTest is ERC20HelperContract { neutralPrice: 8.596021534820399875 * 1e18 }) ); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 8_196.162962286455648823 * 1e18, + borrowerCollateral: 1_000 * 1e18, + borrowert0Np: 8.471136974495192174 * 1e18, + borrowerCollateralization: 0.000000012180856255 * 1e18 + }); - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 8_196.162962286455648823 * 1e18, - borrowerCollateral: 1_000 * 1e18, - borrowert0Np: 8.471136974495192174 * 1e18, - borrowerCollateralization: 0.000000012180856255 * 1e18 - } - ); - - _take( - { - from: _lender, - borrower: _borrower2, - maxCollateral: 1_000.0 * 1e18, - bondChange: 88.990371346118344167 * 1e18, - givenAmount: 595.579761213908032000 * 1e18, - collateralTaken: 1_000 * 1e18, - isReward: true - } - ); + _take({ + from: _lender, + borrower: _borrower2, + maxCollateral: 1_000.0 * 1e18, + bondChange: 88.990371346118344167 * 1e18, + givenAmount: 595.579761213908032000 * 1e18, + collateralTaken: 1_000 * 1e18, + isReward: true + }); _assertPool( PoolParams({ @@ -587,16 +505,13 @@ contract ERC20PoolLiquidationsMiscTest is ERC20HelperContract { interestRateUpdate: block.timestamp - 10 hours }) ); - - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 8_263.304979778717856240 * 1e18, - borrowerCollateral: 0, - borrowert0Np: 8.471136974495192174 * 1e18, - borrowerCollateralization: 0 - } - ); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 8_263.304979778717856240 * 1e18, + borrowerCollateral: 0, + borrowert0Np: 8.471136974495192174 * 1e18, + borrowerCollateralization: 0 + }); _assertRemoveLiquidityAuctionNotClearedRevert({ from: _lender, @@ -604,14 +519,12 @@ contract ERC20PoolLiquidationsMiscTest is ERC20HelperContract { index: _i9_52 }); - _settle( - { - from: _lender, - borrower: _borrower2, - maxDepth: 10, - settledDebt: 7_468.035011263740962170 * 1e18 - } - ); + _settle({ + from: _lender, + borrower: _borrower2, + maxDepth: 10, + settledDebt: 7_468.035011263740962170 * 1e18 + }); _assertPool( PoolParams({ @@ -630,51 +543,40 @@ contract ERC20PoolLiquidationsMiscTest is ERC20HelperContract { interestRateUpdate: block.timestamp - 10 hours }) ); - - _assertBucket( - { - index: _i9_91, - lpBalance: 0, - collateral: 0, - deposit: 0, - exchangeRate: 1 * 1e27 - } - ); - _assertBucket( - { - index: _i9_81, - lpBalance: 0, - collateral: 0, - deposit: 0, - exchangeRate: 1 * 1e27 - } - ); - _assertBucket( - { - index: _i9_72, - lpBalance: 0, - collateral: 0, - deposit: 0, - exchangeRate: 1 * 1e27 - } - ); - _assertBucket( - { - index: _i9_62, - lpBalance: 0, - collateral: 0, - deposit: 0, - exchangeRate: 1 * 1e27 - } - ); - _assertBucket( - { - index: _i9_52, - lpBalance: 0 * 1e27, - collateral: 0, - deposit: 0, - exchangeRate: 1 * 1e27 - } - ); + _assertBucket({ + index: _i9_91, + lpBalance: 0, + collateral: 0, + deposit: 0, + exchangeRate: 1 * 1e18 + }); + _assertBucket({ + index: _i9_81, + lpBalance: 0, + collateral: 0, + deposit: 0, + exchangeRate: 1 * 1e18 + }); + _assertBucket({ + index: _i9_72, + lpBalance: 0, + collateral: 0, + deposit: 0, + exchangeRate: 1 * 1e18 + }); + _assertBucket({ + index: _i9_62, + lpBalance: 0, + collateral: 0, + deposit: 0, + exchangeRate: 1 * 1e18 + }); + _assertBucket({ + index: _i9_52, + lpBalance: 0, + collateral: 0, + deposit: 0, + exchangeRate: 1 * 1e18 + }); } } diff --git a/tests/forge/ERC20Pool/ERC20PoolLiquidationsScaled.t.sol b/tests/forge/ERC20Pool/ERC20PoolLiquidationsScaled.t.sol index 98fac1dbc..dcf7813db 100644 --- a/tests/forge/ERC20Pool/ERC20PoolLiquidationsScaled.t.sol +++ b/tests/forge/ERC20Pool/ERC20PoolLiquidationsScaled.t.sol @@ -73,15 +73,15 @@ contract ERC20PoolLiquidationsScaledTest is ERC20DSTestPlus { // deposit 200k quote token across 4 buckets uint256 lpBalance; for (uint i=0; i<4; ++i) { - _addInitialLiquidity( - { + + _addInitialLiquidity({ from: _lender, amount: 50_000 * 1e18, index: startBucketId + i - } - ); + }); + (lpBalance, ) = _pool.lenderInfo(startBucketId + i, _lender); - assertEq(lpBalance, 50_000 * 1e27); + assertEq(lpBalance, 50_000 * 1e18); } assertEq(_pool.depositSize(), 200_000 * 1e18); } @@ -295,6 +295,7 @@ contract ERC20PoolLiquidationsScaledTest is ERC20DSTestPlus { , , ) = _pool.auctionInfo(_borrower); + assertEq(auctionKicker, kicker); assertGe(auctionBondFactor, 0.01 * 1e18); assertLe(auctionBondFactor, 0.3 * 1e18); diff --git a/tests/forge/ERC20Pool/ERC20PoolLiquidationsSettle.t.sol b/tests/forge/ERC20Pool/ERC20PoolLiquidationsSettle.t.sol index a37269ff1..0cea6fdc9 100644 --- a/tests/forge/ERC20Pool/ERC20PoolLiquidationsSettle.t.sol +++ b/tests/forge/ERC20Pool/ERC20PoolLiquidationsSettle.t.sol @@ -26,75 +26,57 @@ contract ERC20PoolLiquidationsSettleTest is ERC20HelperContract { _mintCollateralAndApproveTokens(_lender1, 4 * 1e18); // Lender adds Quote token accross 5 prices - _addInitialLiquidity( - { - from: _lender, - amount: 2_000 * 1e18, - index: _i9_91 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 5_000 * 1e18, - index: _i9_81 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 11_000 * 1e18, - index: _i9_72 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 25_000 * 1e18, - index: _i9_62 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 30_000 * 1e18, - index: _i9_52 - } - ); + _addInitialLiquidity({ + from: _lender, + amount: 2_000 * 1e18, + index: _i9_91 + }); + _addInitialLiquidity({ + from: _lender, + amount: 5_000 * 1e18, + index: _i9_81 + }); + _addInitialLiquidity({ + from: _lender, + amount: 11_000 * 1e18, + index: _i9_72 + }); + _addInitialLiquidity({ + from: _lender, + amount: 25_000 * 1e18, + index: _i9_62 + }); + _addInitialLiquidity({ + from: _lender, + amount: 30_000 * 1e18, + index: _i9_52 + }); // first borrower adds collateral token and borrows - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - amount: 2 * 1e18 - } - ); - _borrow( - { - from: _borrower, - amount: 19.25 * 1e18, - indexLimit: _i9_91, - newLup: 9.917184843435912074 * 1e18 - } - ); + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + amount: 2 * 1e18 + }); + _borrow({ + from: _borrower, + amount: 19.25 * 1e18, + indexLimit: _i9_91, + newLup: 9.917184843435912074 * 1e18 + }); // second borrower adds collateral token and borrows - _pledgeCollateral( - { - from: _borrower2, - borrower: _borrower2, - amount: 1_000 * 1e18 - } - ); - _borrow( - { - from: _borrower2, - amount: 7_980 * 1e18, - indexLimit: _i9_72, - newLup: 9.721295865031779605 * 1e18 - } - ); + _pledgeCollateral({ + from: _borrower2, + borrower: _borrower2, + amount: 1_000 * 1e18 + }); + _borrow({ + from: _borrower2, + amount: 7_980 * 1e18, + indexLimit: _i9_72, + newLup: 9.721295865031779605 * 1e18 + }); /*****************************/ /*** Assert pre-kick state ***/ @@ -117,47 +99,40 @@ contract ERC20PoolLiquidationsSettleTest is ERC20HelperContract { interestRateUpdate: _startTime }) ); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 19.268509615384615394 * 1e18, - borrowerCollateral: 2 * 1e18, - borrowert0Np: 10.115967548076923081 * 1e18, - borrowerCollateralization: 1.009034539679184679 * 1e18 - } - ); - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 7_987.673076923076926760 * 1e18, - borrowerCollateral: 1_000 * 1e18, - borrowert0Np: 8.471136974495192174 * 1e18, - borrowerCollateralization: 1.217037273735858713 * 1e18 - } - ); - _assertReserveAuction( - { - reserves: 7.691586538461542154 * 1e18, - claimableReserves : 0, - claimableReservesRemaining: 0, - auctionPrice: 0, - timeRemaining: 0 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 19.268509615384615394 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 10.115967548076923081 * 1e18, + borrowerCollateralization: 1.009034539679184679 * 1e18 + }); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 7_987.673076923076926760 * 1e18, + borrowerCollateral: 1_000 * 1e18, + borrowert0Np: 8.471136974495192174 * 1e18, + borrowerCollateralization: 1.217037273735858713 * 1e18 + }); + _assertReserveAuction({ + reserves: 7.691586538461542154 * 1e18, + claimableReserves : 0, + claimableReservesRemaining: 0, + auctionPrice: 0, + timeRemaining: 0 + }); + assertEq(_quote.balanceOf(_lender), 47_000 * 1e18); } function testSettleOnAuctionKicked72HoursAgoAndPartiallyTaken() external tearDown { // Borrower2 borrows - _borrow( - { - from: _borrower2, - amount: 1_730 * 1e18, - indexLimit: _i9_72, - newLup: 9.721295865031779605 * 1e18 - } - ); + _borrow({ + from: _borrower2, + amount: 1_730 * 1e18, + indexLimit: _i9_72, + newLup: 9.721295865031779605 * 1e18 + }); // Skip to make borrower undercollateralized skip(100 days); @@ -178,27 +153,22 @@ contract ERC20PoolLiquidationsSettleTest is ERC20HelperContract { neutralPrice: 0 }) ); - - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 9_853.394241979221645666 * 1e18, - borrowerCollateral: 1_000 * 1e18, - borrowert0Np: 10.307611531622595991 * 1e18, - borrowerCollateralization: 0.986593617011217057 * 1e18 - } - ); - - _kick( - { - from: _lender, - borrower: _borrower2, - debt: 9_976.561670003961916237 * 1e18, - collateral: 1_000 * 1e18, - bond: 98.533942419792216457 * 1e18, - transferAmount: 98.533942419792216457 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 9_853.394241979221645666 * 1e18, + borrowerCollateral: 1_000 * 1e18, + borrowert0Np: 10.307611531622595991 * 1e18, + borrowerCollateralization: 0.986593617011217057 * 1e18 + }); + + _kick({ + from: _lender, + borrower: _borrower2, + debt: 9_976.561670003961916237 * 1e18, + collateral: 1_000 * 1e18, + bond: 98.533942419792216457 * 1e18, + transferAmount: 98.533942419792216457 * 1e18 + }); _assertAuction( AuctionParams({ @@ -216,51 +186,41 @@ contract ERC20PoolLiquidationsSettleTest is ERC20HelperContract { neutralPrice: 10.449783245217816340 * 1e18 }) ); - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 9_976.561670003961916237 * 1e18, - borrowerCollateral: 1_000 * 1e18, - borrowert0Np: 10.307611531622595991 * 1e18, - borrowerCollateralization: 0.974413448899967463 * 1e18 - } - ); - _assertBucket( - { - index: _i9_91, - lpBalance: 2_000 * 1e27, - collateral: 0, - deposit: 2_118.781595119199960000 * 1e18, - exchangeRate: 1.05939079755959998 * 1e27 - } - ); - _assertBucket( - { - index: _i9_81, - lpBalance: 5_000 * 1e27, - collateral: 0, - deposit: 5_000 * 1e18, - exchangeRate: 1 * 1e27 - } - ); - _assertBucket( - { - index: _i9_72, - lpBalance: 11_000 * 1e27, - collateral: 0, - deposit: 11_000 * 1e18, - exchangeRate: 1 * 1e27 - } - ); - _assertBucket( - { - index: _i9_62, - lpBalance: 25_000 * 1e27, - collateral: 0, - deposit: 25_000 * 1e18, - exchangeRate: 1 * 1e27 - } - ); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 9_976.561670003961916237 * 1e18, + borrowerCollateral: 1_000 * 1e18, + borrowert0Np: 10.307611531622595991 * 1e18, + borrowerCollateralization: 0.974413448899967463 * 1e18 + }); + _assertBucket({ + index: _i9_91, + lpBalance: 2_000 * 1e18, + collateral: 0, + deposit: 2_118.781595119199960000 * 1e18, + exchangeRate: 1.05939079755959998 * 1e18 + }); + _assertBucket({ + index: _i9_81, + lpBalance: 5_000 * 1e18, + collateral: 0, + deposit: 5_000 * 1e18, + exchangeRate: 1 * 1e18 + }); + _assertBucket({ + index: _i9_72, + lpBalance: 11_000 * 1e18, + collateral: 0, + deposit: 11_000 * 1e18, + exchangeRate: 1 * 1e18 + }); + _assertBucket({ + index: _i9_62, + lpBalance: 25_000 * 1e18, + collateral: 0, + deposit: 25_000 * 1e18, + exchangeRate: 1 * 1e18 + }); // skip ahead so take can be called on the loan skip(10 hours); @@ -281,29 +241,25 @@ contract ERC20PoolLiquidationsSettleTest is ERC20HelperContract { neutralPrice: 10.449783245217816340 * 1e18 }) ); - - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 9_977.074177773911990381 * 1e18, - borrowerCollateral: 1_000 * 1e18, - borrowert0Np: 10.307611531622595991 * 1e18, - borrowerCollateralization: 0.974363394700228467 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 9_977.074177773911990381 * 1e18, + borrowerCollateral: 1_000 * 1e18, + borrowert0Np: 10.307611531622595991 * 1e18, + borrowerCollateralization: 0.974363394700228467 * 1e18 + }); // take partial 800 collateral - _take( - { - from: _lender, - borrower: _borrower2, - maxCollateral: 800 * 1e18, - bondChange: 5.224891622608908288 * 1e18, - givenAmount: 522.489162260890828800 * 1e18, - collateralTaken: 800 * 1e18, - isReward: true - } - ); + _take({ + from: _lender, + borrower: _borrower2, + maxCollateral: 800 * 1e18, + bondChange: 5.224891622608908288 * 1e18, + givenAmount: 522.489162260890828800 * 1e18, + collateralTaken: 800 * 1e18, + isReward: true + }); + _assertAuction( AuctionParams({ borrower: _borrower2, @@ -320,83 +276,66 @@ contract ERC20PoolLiquidationsSettleTest is ERC20HelperContract { neutralPrice: 10.449783245217816340 * 1e18 }) ); - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 10_158.205099579803908908 * 1e18, - borrowerCollateral: 200 * 1e18, - borrowert0Np: 10.307611531622595991 * 1e18, - borrowerCollateralization: 0.191397904841159446 * 1e18 - } - ); - _assertBucket( - { - index: _i9_91, - lpBalance: 2_000 * 1e27, - collateral: 0, - deposit: 2_118.911507166546112000 * 1e18, - exchangeRate: 1.059455753583273056000 * 1e27 - } - ); - _assertBucket( - { - index: _i9_81, - lpBalance: 5_000 * 1e27, - collateral: 0, - deposit: 5_000.306572531226000000 * 1e18, - exchangeRate: 1.0000613145062452 * 1e27 - } - ); - _assertBucket( - { - index: _i9_72, - lpBalance: 11_000 * 1e27, - collateral: 0, - deposit: 11_000 * 1e18, - exchangeRate: 1 * 1e27 - } - ); - _assertBucket( - { - index: _i9_62, - lpBalance: 25_000 * 1e27, - collateral: 0, - deposit: 25_000 * 1e18, - exchangeRate: 1 * 1e27 - } - ); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 10_158.205099579803908908 * 1e18, + borrowerCollateral: 200 * 1e18, + borrowert0Np: 10.307611531622595991 * 1e18, + borrowerCollateralization: 0.191397904841159446 * 1e18 + }); + _assertBucket({ + index: _i9_91, + lpBalance: 2_000 * 1e18, + collateral: 0, + deposit: 2_118.911507166546112000 * 1e18, + exchangeRate: 1.059455753583273056 * 1e18 + }); + _assertBucket({ + index: _i9_81, + lpBalance: 5_000 * 1e18, + collateral: 0, + deposit: 5_000.306572531226000000 * 1e18, + exchangeRate: 1.0000613145062452 * 1e18 + }); + _assertBucket({ + index: _i9_72, + lpBalance: 11_000 * 1e18, + collateral: 0, + deposit: 11_000 * 1e18, + exchangeRate: 1 * 1e18 + }); + _assertBucket({ + index: _i9_62, + lpBalance: 25_000 * 1e18, + collateral: 0, + deposit: 25_000 * 1e18, + exchangeRate: 1 * 1e18 + }); // settle should affect first 3 buckets, reducing deposit and incrementing collateral skip(73 hours); - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 10_162.015140830231868753 * 1e18, - borrowerCollateral: 200 * 1e18, - borrowert0Np: 10.307611531622595991 * 1e18, - borrowerCollateralization: 0.191326144082827145 * 1e18 - } - ); - - _assertBucket( - { - index: _i9_91, - lpBalance: 2_000 * 1e27, - collateral: 0, - deposit: 2_118.911507166546112000 * 1e18, - exchangeRate: 1.059455753583273056000 * 1e27 - } - ); - - _settle( - { - from: _lender, - borrower: _borrower2, - maxDepth: 10, - settledDebt: 10_019.485661146575724663 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 10_162.015140830231868753 * 1e18, + borrowerCollateral: 200 * 1e18, + borrowert0Np: 10.307611531622595991 * 1e18, + borrowerCollateralization: 0.191326144082827145 * 1e18 + }); + _assertBucket({ + index: _i9_91, + lpBalance: 2_000 * 1e18, + collateral: 0, + deposit: 2_118.911507166546112000 * 1e18, + exchangeRate: 1.059455753583273056 * 1e18 + }); + + _settle({ + from: _lender, + borrower: _borrower2, + maxDepth: 10, + settledDebt: 10_019.485661146575724663 * 1e18 + }); _assertAuction( AuctionParams({ @@ -414,54 +353,41 @@ contract ERC20PoolLiquidationsSettleTest is ERC20HelperContract { neutralPrice: 0 }) ); - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 0, - borrowerCollateral: 0, - borrowert0Np: 10.307611531622595991 * 1e18, - borrowerCollateralization: 1 * 1e18 - } - ); - _assertBucket( - { - index: _i9_91, - lpBalance: 2_000 * 1e27, - collateral: 200 * 1e18, - deposit: 0, - exchangeRate: 0.9917184843435912074 * 1e27 - } - ); - _assertBucket( - { - index: _i9_81, - lpBalance: 0, - collateral: 0, - deposit: 0, - exchangeRate: 1 * 1e27 - } - ); - - _assertBucket( - { - index: _i9_72, - lpBalance: 11_000 * 1e27, - collateral: 0, - deposit: 8_807.325768325035155556 * 1e18, - exchangeRate: 0.800665978938639559596000000 * 1e27 - } - ); - - _assertBucket( - { - index: _i9_62, - lpBalance: 25_000 * 1e27, - collateral: 0, - deposit: 25_000 * 1e18, - exchangeRate: 1 * 1e27 - } - ); - + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 0, + borrowerCollateral: 0, + borrowert0Np: 10.307611531622595991 * 1e18, + borrowerCollateralization: 1 * 1e18 + }); + _assertBucket({ + index: _i9_91, + lpBalance: 2_000 * 1e18, + collateral: 200 * 1e18, + deposit: 0, + exchangeRate: 0.991718484343591207 * 1e18 + }); + _assertBucket({ + index: _i9_81, + lpBalance: 0, + collateral: 0, + deposit: 0, + exchangeRate: 1 * 1e18 + }); + _assertBucket({ + index: _i9_72, + lpBalance: 11_000 * 1e18, + collateral: 0, + deposit: 8_807.325768325035155556 * 1e18, + exchangeRate: 0.800665978938639560 * 1e18 + }); + _assertBucket({ + index: _i9_62, + lpBalance: 25_000 * 1e18, + collateral: 0, + deposit: 25_000 * 1e18, + exchangeRate: 1 * 1e18 + }); _assertPool( PoolParams({ htp: 9.910303333009215085 * 1e18, @@ -483,14 +409,12 @@ contract ERC20PoolLiquidationsSettleTest is ERC20HelperContract { function testSettleOnAuctionKicked72HoursAgo() external tearDown { // Borrower2 borrows - _borrow( - { - from: _borrower2, - amount: 1_730 * 1e18, - indexLimit: _i9_72, - newLup: 9.721295865031779605 * 1e18 - } - ); + _borrow({ + from: _borrower2, + amount: 1_730 * 1e18, + indexLimit: _i9_72, + newLup: 9.721295865031779605 * 1e18 + }); // Skip to make borrower undercollateralized skip(100 days); @@ -511,27 +435,23 @@ contract ERC20PoolLiquidationsSettleTest is ERC20HelperContract { neutralPrice: 0 }) ); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 9_853.394241979221645666 * 1e18, + borrowerCollateral: 1_000 * 1e18, + borrowert0Np: 10.307611531622595991 * 1e18, + borrowerCollateralization: 0.986593617011217057 * 1e18 + }); + + _kick({ + from: _lender, + borrower: _borrower2, + debt: 9_976.561670003961916237 * 1e18, + collateral: 1_000 * 1e18, + bond: 98.533942419792216457 * 1e18, + transferAmount: 98.533942419792216457 * 1e18 + }); - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 9_853.394241979221645666 * 1e18, - borrowerCollateral: 1_000 * 1e18, - borrowert0Np: 10.307611531622595991 * 1e18, - borrowerCollateralization: 0.986593617011217057 * 1e18 - } - ); - - _kick( - { - from: _lender, - borrower: _borrower2, - debt: 9_976.561670003961916237 * 1e18, - collateral: 1_000 * 1e18, - bond: 98.533942419792216457 * 1e18, - transferAmount: 98.533942419792216457 * 1e18 - } - ); _assertAuction( AuctionParams({ borrower: _borrower2, @@ -548,51 +468,41 @@ contract ERC20PoolLiquidationsSettleTest is ERC20HelperContract { neutralPrice: 10.449783245217816340 * 1e18 }) ); - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 9_976.561670003961916237 * 1e18, - borrowerCollateral: 1_000 * 1e18, - borrowert0Np: 10.307611531622595991 * 1e18, - borrowerCollateralization: 0.974413448899967463 * 1e18 - } - ); - _assertBucket( - { - index: _i9_91, - lpBalance: 2_000 * 1e27, - collateral: 0, - deposit: 2_118.781595119199960000 * 1e18, - exchangeRate: 1.05939079755959998 * 1e27 - } - ); - _assertBucket( - { - index: _i9_81, - lpBalance: 5_000 * 1e27, - collateral: 0, - deposit: 5_000 * 1e18, - exchangeRate: 1 * 1e27 - } - ); - _assertBucket( - { - index: _i9_72, - lpBalance: 11_000 * 1e27, - collateral: 0, - deposit: 11_000 * 1e18, - exchangeRate: 1 * 1e27 - } - ); - _assertBucket( - { - index: _i9_62, - lpBalance: 25_000 * 1e27, - collateral: 0, - deposit: 25_000 * 1e18, - exchangeRate: 1 * 1e27 - } - ); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 9_976.561670003961916237 * 1e18, + borrowerCollateral: 1_000 * 1e18, + borrowert0Np: 10.307611531622595991 * 1e18, + borrowerCollateralization: 0.974413448899967463 * 1e18 + }); + _assertBucket({ + index: _i9_91, + lpBalance: 2_000 * 1e18, + collateral: 0, + deposit: 2_118.781595119199960000 * 1e18, + exchangeRate: 1.05939079755959998 * 1e18 + }); + _assertBucket({ + index: _i9_81, + lpBalance: 5_000 * 1e18, + collateral: 0, + deposit: 5_000 * 1e18, + exchangeRate: 1 * 1e18 + }); + _assertBucket({ + index: _i9_72, + lpBalance: 11_000 * 1e18, + collateral: 0, + deposit: 11_000 * 1e18, + exchangeRate: 1 * 1e18 + }); + _assertBucket({ + index: _i9_62, + lpBalance: 25_000 * 1e18, + collateral: 0, + deposit: 25_000 * 1e18, + exchangeRate: 1 * 1e18 + }); // settle should work on an kicked auction if 72 hours passed from kick time // settle should affect first 3 buckets, reducing deposit and incrementing collateral @@ -614,25 +524,21 @@ contract ERC20PoolLiquidationsSettleTest is ERC20HelperContract { neutralPrice: 10.449783245217816340 * 1e18 }) ); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 9_980.303582194898667001 * 1e18, + borrowerCollateral: 1_000 * 1e18, + borrowert0Np: 10.307611531622595991 * 1e18, + borrowerCollateralization: 0.974048112361512224 * 1e18 + }); + + _settle({ + from: _lender, + borrower: _borrower2, + maxDepth: 10, + settledDebt: 9_840.828245192307696845 * 1e18 + }); - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 9_980.303582194898667001 * 1e18, - borrowerCollateral: 1_000 * 1e18, - borrowert0Np: 10.307611531622595991 * 1e18, - borrowerCollateralization: 0.974048112361512224 * 1e18 - } - ); - - _settle( - { - from: _lender, - borrower: _borrower2, - maxDepth: 10, - settledDebt: 9_840.828245192307696845 * 1e18 - } - ); _assertAuction( AuctionParams({ borrower: _borrower2, @@ -649,85 +555,71 @@ contract ERC20PoolLiquidationsSettleTest is ERC20HelperContract { neutralPrice: 0 }) ); - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 0, - borrowerCollateral: 0, - borrowert0Np: 10.307611531622595991 * 1e18, - borrowerCollateralization: 1 * 1e18 - } - ); - _assertBucket( - { - index: _i9_91, - lpBalance: 2_000 * 1e27, - collateral: 213.743127712733065764 * 1e18, - deposit: 0, - exchangeRate: 1.059865053270651414002083680 * 1e27 - } - ); - _assertBucket( - { - index: _i9_81, - lpBalance: 5_000 * 1e27, - collateral: 509.457659688392150697 * 1e18, - deposit: 0, - exchangeRate: 1.000447668331784572999225097 * 1e27 - } - ); - _assertBucket( - { - index: _i9_72, - lpBalance: 11_000 * 1e27, - collateral: 276.799212598874783539 * 1e18, - deposit: 8_289.734142970131967959 * 1e18, - exchangeRate: 0.998234653077420534042741275 * 1e27 - } - ); - _assertBucket( - { - index: _i9_62, - lpBalance: 25_000 * 1e27, - collateral: 0, - deposit: 25_000 * 1e18, - exchangeRate: 1 * 1e27 - } - ); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 0, + borrowerCollateral: 0, + borrowert0Np: 10.307611531622595991 * 1e18, + borrowerCollateralization: 1 * 1e18 + }); + _assertBucket({ + index: _i9_91, + lpBalance: 2_000 * 1e18, + collateral: 213.743127712733065764 * 1e18, + deposit: 0, + exchangeRate: 1.059865053270651414 * 1e18 + }); + _assertBucket({ + index: _i9_81, + lpBalance: 5_000 * 1e18, + collateral: 509.457659688392150697 * 1e18, + deposit: 0, + exchangeRate: 1.000447668331784573 * 1e18 + }); + _assertBucket({ + index: _i9_72, + lpBalance: 11_000 * 1e18, + collateral: 276.799212598874783539 * 1e18, + deposit: 8_289.734142970131967959 * 1e18, + exchangeRate: 0.998234653077420534 * 1e18 + }); + _assertBucket({ + index: _i9_62, + lpBalance: 25_000 * 1e18, + collateral: 0, + deposit: 25_000 * 1e18, + exchangeRate: 1 * 1e18 + }); } function testSettleAuctionReverts() external tearDown { // Borrower2 borrows - _borrow( - { - from: _borrower2, - amount: 1_730 * 1e18, - indexLimit: _i9_72, - newLup: 9.721295865031779605 * 1e18 - } - ); + _borrow({ + from: _borrower2, + amount: 1_730 * 1e18, + indexLimit: _i9_72, + newLup: 9.721295865031779605 * 1e18 + }); // Skip to make borrower undercollateralized skip(100 days); // settle should revert on a borrower that is not auctioned - _assertSettleOnNotKickedAuctionRevert( - { - from: _lender, - borrower: _borrower2 - } - ); + _assertSettleOnNotKickedAuctionRevert({ + from: _lender, + borrower: _borrower2 + }); uint256 kickTime = _startTime + 100 days; - _kick( - { - from: _lender, - borrower: _borrower2, - debt: 9_976.561670003961916237 * 1e18, - collateral: 1_000 * 1e18, - bond: 98.533942419792216457 * 1e18, - transferAmount: 98.533942419792216457 * 1e18 - } - ); + + _kick({ + from: _lender, + borrower: _borrower2, + debt: 9_976.561670003961916237 * 1e18, + collateral: 1_000 * 1e18, + bond: 98.533942419792216457 * 1e18, + transferAmount: 98.533942419792216457 * 1e18 + }); + _assertAuction( AuctionParams({ borrower: _borrower2, @@ -744,23 +636,20 @@ contract ERC20PoolLiquidationsSettleTest is ERC20HelperContract { neutralPrice: 10.449783245217816340 * 1e18 }) ); - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 9_976.561670003961916237 * 1e18, - borrowerCollateral: 1_000 * 1e18, - borrowert0Np: 10.307611531622595991 * 1e18, - borrowerCollateralization: 0.974413448899967463 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 9_976.561670003961916237 * 1e18, + borrowerCollateral: 1_000 * 1e18, + borrowert0Np: 10.307611531622595991 * 1e18, + borrowerCollateralization: 0.974413448899967463 * 1e18 + }); // settle should revert on an kicked auction but 72 hours not passed (there's still debt to settle and collateral to be auctioned) - _assertSettleOnNotClearableAuctionRevert( - { - from: _lender, - borrower: _borrower2 - } - ); + _assertSettleOnNotClearableAuctionRevert({ + from: _lender, + borrower: _borrower2 + }); + // skip ahead so take can be called on the loan skip(10 hours); @@ -780,218 +669,208 @@ contract ERC20PoolLiquidationsSettleTest is ERC20HelperContract { neutralPrice: 10.449783245217816340 * 1e18 }) ); - - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 9_977.074177773911990381 * 1e18, - borrowerCollateral: 1_000 * 1e18, - borrowert0Np: 10.307611531622595991 * 1e18, - borrowerCollateralization: 0.974363394700228467 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 9_977.074177773911990381 * 1e18, + borrowerCollateral: 1_000 * 1e18, + borrowert0Np: 10.307611531622595991 * 1e18, + borrowerCollateralization: 0.974363394700228467 * 1e18 + }); // take entire collateral - _take( - { - from: _lender, - borrower: _borrower2, - maxCollateral: 1_000 * 1e18, - bondChange: 6.531114528261135360 * 1e18, - givenAmount: 653.111452826113536000 * 1e18, - collateralTaken: 1_000 * 1e18, - isReward: true - } - ); + _take({ + from: _lender, + borrower: _borrower2, + maxCollateral: 1_000 * 1e18, + bondChange: 6.531114528261135360 * 1e18, + givenAmount: 653.111452826113536000 * 1e18, + collateralTaken: 1_000 * 1e18, + isReward: true + }); // remove quote tokens should fail since auction head is clearable - _assertRemoveLiquidityAuctionNotClearedRevert( - { - from: _lender, - amount: 1_000 * 1e18, - index: _i9_52 - } - ); - _assertRemoveAllLiquidityAuctionNotClearedRevert( - { - from: _lender, - index: _i9_52 - } - ); + _assertRemoveLiquidityAuctionNotClearedRevert({ + from: _lender, + amount: 1_000 * 1e18, + index: _i9_52 + }); + + _assertRemoveAllLiquidityAuctionNotClearedRevert({ + from: _lender, + index: _i9_52 + }); + // remove collateral should fail since auction head is clearable - _assertRemoveCollateralAuctionNotClearedRevert( - { - from: _lender, - amount: 10 * 1e18, - index: _i9_52 - } - ); + _assertRemoveCollateralAuctionNotClearedRevert({ + from: _lender, + amount: 10 * 1e18, + index: _i9_52 + }); // remove all collateral should fail since auction head is clearable - _assertRemoveAllCollateralAuctionNotClearedRevert( - { - from: _lender, - index: _i9_52 - } - ); + _assertRemoveAllCollateralAuctionNotClearedRevert({ + from: _lender, + index: _i9_52 + }); // add liquidity in same block should be possible as debt was not yet settled / bucket is not yet insolvent - _addLiquidity( - { - from: _lender1, - amount: 100 * 1e18, - index: _i9_91, - lpAward: 94.388085261495553046979329248 * 1e27, - newLup: 9.721295865031779605 * 1e18 - } - ); - _assertLenderLpBalance( - { - lender: _lender1, - index: _i9_91, - lpBalance: 94.388085261495553046979329248 * 1e27, - depositTime: _startTime + 100 days + 10 hours - } - ); + _addLiquidity({ + from: _lender1, + amount: 100 * 1e18, + index: _i9_91, + lpAward: 94.388085261495553047 * 1e18, + newLup: 9.721295865031779605 * 1e18 + }); + + _assertLenderLpBalance({ + lender: _lender1, + index: _i9_91, + lpBalance: 94.388085261495553047 * 1e18, + depositTime: _startTime + 100 days + 10 hours + }); + // adding to a different bucket for testing move in same block with bucket bankruptcy - _addLiquidity( - { - from: _lender1, - amount: 100 * 1e18, - index: _i9_52, - lpAward: 100 * 1e27, - newLup: 9.721295865031779605 * 1e18 - } - ); + _addLiquidity({ + from: _lender1, + amount: 100 * 1e18, + index: _i9_52, + lpAward: 100 * 1e18, + newLup: 9.721295865031779605 * 1e18 + }); // settle to make buckets insolvent // settle should work because there is still debt to settle but no collateral left to auction (even if 72 hours didn't pass from kick) - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 10_028.889031920233428707 * 1e18, - borrowerCollateral: 0, - borrowert0Np: 10.307611531622595991 * 1e18, - borrowerCollateralization: 0 - } - ); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 10_028.889031920233428707 * 1e18, + borrowerCollateral: 0, + borrowert0Np: 10.307611531622595991 * 1e18, + borrowerCollateralization: 0 + }); assertTrue(block.timestamp - kickTime < 72 hours); // assert auction was kicked less than 72 hours ago - _settle( - { - from: _lender, - borrower: _borrower2, - maxDepth: 10, - settledDebt: 9_891.935520844277346922 * 1e18 - } - ); + + _settle({ + from: _lender, + borrower: _borrower2, + maxDepth: 10, + settledDebt: 9_891.935520844277346922 * 1e18 + }); // bucket is insolvent, balances are resetted - _assertBucket( - { - index: _i9_91, - lpBalance: 0, // bucket is bankrupt - collateral: 0, - deposit: 0, - exchangeRate: 1 * 1e27 - } - ); + _assertBucket({ + index: _i9_91, + lpBalance: 0, // bucket is bankrupt + collateral: 0, + deposit: 0, + exchangeRate: 1 * 1e18 + }); // after bucket bankruptcy lenders balance is zero - _assertLenderLpBalance( - { - lender: _lender, - index: _i9_91, - lpBalance: 0, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: _lender1, - index: _i9_91, - lpBalance: 0, - depositTime: _startTime + 100 days + 10 hours - } - ); + _assertLenderLpBalance({ + lender: _lender, + index: _i9_91, + lpBalance: 0, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: _lender1, + index: _i9_91, + lpBalance: 0, + depositTime: _startTime + 100 days + 10 hours + }); + // cannot add liquidity in same block when bucket marked insolvent - _assertAddLiquidityBankruptcyBlockRevert( - { - from: _lender1, - amount: 1_000 * 1e18, - index: _i9_91 - } - ); + _assertAddLiquidityBankruptcyBlockRevert({ + from: _lender1, + amount: 1_000 * 1e18, + index: _i9_91 + }); + // cannot add collateral in same block when bucket marked insolvent - _assertAddCollateralBankruptcyBlockRevert( - { - from: _lender1, - amount: 10 * 1e18, - index: _i9_91 - } - ); + _assertAddCollateralBankruptcyBlockRevert({ + from: _lender1, + amount: 10 * 1e18, + index: _i9_91 + }); + // cannot move LPs in same block when bucket marked insolvent - _assertMoveLiquidityBankruptcyBlockRevert( - { - from: _lender1, - amount: 10 * 1e18, - fromIndex: _i9_52, - toIndex: _i9_91 - } - ); + _assertMoveLiquidityBankruptcyBlockRevert({ + from: _lender1, + amount: 10 * 1e18, + fromIndex: _i9_52, + toIndex: _i9_91 + }); // all operations should work if not in same block skip(1 hours); - _pool.addQuoteToken(100 * 1e18, _i9_91); - _pool.moveQuoteToken(10 * 1e18, _i9_52, _i9_91); - ERC20Pool(address(_pool)).addCollateral(4 * 1e18, _i9_91); - _assertLenderLpBalance( - { - lender: _lender1, - index: _i9_91, - lpBalance: 149.668739373743648296000000000 * 1e27, - depositTime: _startTime + 100 days + 10 hours + 1 hours - } - ); + + _pool.addQuoteToken(100 * 1e18, _i9_91, block.timestamp + 1 minutes); + _pool.moveQuoteToken(10 * 1e18, _i9_52, _i9_91, block.timestamp + 1 minutes); + ERC20Pool(address(_pool)).addCollateral(4 * 1e18, _i9_91, block.timestamp + 1 minutes); + + _assertLenderLpBalance({ + lender: _lender1, + index: _i9_91, + lpBalance: 149.668739373743648296 * 1e18, + depositTime: _startTime + 100 days + 10 hours + 1 hours + }); // bucket is healthy again - _assertBucket( - { - index: _i9_91, - lpBalance: 149.668739373743648296000000000 * 1e27, - collateral: 4 * 1e18, - deposit: 110.0000000000000000000000 * 1e18, - exchangeRate: 1.00000000000000000000000000 * 1e27 - } - ); + _assertBucket({ + index: _i9_91, + lpBalance: 149.668739373743648296 * 1e18, + collateral: 4 * 1e18, + deposit: 110 * 1e18, + exchangeRate: 1 * 1e18 + }); // when moving to a bucket that was marked insolvent, the deposit time should be the greater between from bucket deposit time and insolvency time + 1 changePrank(_lender); - _assertLenderLpBalance( - { - lender: _lender, - index: _i9_91, - lpBalance: 0, - depositTime: _startTime - } - ); - _pool.moveQuoteToken(1_000 * 1e18, _i9_52, _i9_91); - _assertLenderLpBalance( - { - lender: _lender, - index: _i9_91, - lpBalance: 1_000.000000000000000000000000000 * 1e27, - depositTime: _startTime + 100 days + 10 hours + 1 // _i9_91 bucket insolvency time + 1 (since deposit in _i9_52 from bucket was done before _i9_91 target bucket become insolvent) - } - ); - _pool.addQuoteToken(1_000 * 1e18, _i9_52); - _pool.moveQuoteToken(1_000 * 1e18, _i9_52, _i9_91); - _assertLenderLpBalance( - { - lender: _lender, - index: _i9_91, - lpBalance: 1_999.999999999999999999130185000 * 1e27, - depositTime: _startTime + 100 days + 10 hours + 1 hours // time of deposit in _i9_52 from bucket (since deposit in _i9_52 from bucket was done after _i9_91 target bucket become insolvent) - } - ); + _assertLenderLpBalance({ + lender: _lender, + index: _i9_91, + lpBalance: 0, + depositTime: _startTime + }); + + _pool.moveQuoteToken(1_000 * 1e18, _i9_52, _i9_91, block.timestamp + 1 minutes); + + _assertLenderLpBalance({ + lender: _lender, + index: _i9_91, + lpBalance: 1_000 * 1e18, + depositTime: _startTime + 100 days + 10 hours + 1 // _i9_91 bucket insolvency time + 1 (since deposit in _i9_52 from bucket was done before _i9_91 target bucket become insolvent) + }); + + _pool.addQuoteToken(1_000 * 1e18, _i9_52, block.timestamp + 1 minutes); + _pool.moveQuoteToken(1_000 * 1e18, _i9_52, _i9_91, block.timestamp + 1 minutes); + + _assertLenderLpBalance({ + lender: _lender, + index: _i9_91, + lpBalance: 2_000 * 1e18, + depositTime: _startTime + 100 days + 10 hours + 1 hours // time of deposit in _i9_52 from bucket (since deposit in _i9_52 from bucket was done after _i9_91 target bucket become insolvent) + }); + + // ensure bucket bankruptcy when moving amounts from an unbalanced bucket leave bucket healthy + _assertBucket({ + index: _i9_72, + lpBalance: 11_000 * 1e18, + collateral: 0 * 1e18, + deposit: 9_035.875749431291350856 * 1e18, + exchangeRate: 0.821443249948299214 * 1e18 + }); + + vm.expectEmit(true, true, false, true); + emit BucketBankruptcy(_i9_72, 3827); + _pool.moveQuoteToken(10000000000 * 1e18, _i9_72, _i9_91, type(uint256).max); + + _assertBucket({ + index: _i9_72, + lpBalance: 0, + collateral: 0 * 1e18, + deposit: 0, + exchangeRate: 1 * 1e18 + }); } } diff --git a/tests/forge/ERC20Pool/ERC20PoolLiquidationsTake.t.sol b/tests/forge/ERC20Pool/ERC20PoolLiquidationsTake.t.sol index 6a539dd8d..db55f6fd8 100644 --- a/tests/forge/ERC20Pool/ERC20PoolLiquidationsTake.t.sol +++ b/tests/forge/ERC20Pool/ERC20PoolLiquidationsTake.t.sol @@ -26,75 +26,57 @@ contract ERC20PoolLiquidationsTakeTest is ERC20HelperContract { _mintCollateralAndApproveTokens(_lender1, 4 * 1e18); // Lender adds Quote token accross 5 prices - _addInitialLiquidity( - { - from: _lender, - amount: 2_000 * 1e18, - index: _i9_91 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 5_000 * 1e18, - index: _i9_81 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 11_000 * 1e18, - index: _i9_72 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 25_000 * 1e18, - index: _i9_62 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 30_000 * 1e18, - index: _i9_52 - } - ); + _addInitialLiquidity({ + from: _lender, + amount: 2_000 * 1e18, + index: _i9_91 + }); + _addInitialLiquidity({ + from: _lender, + amount: 5_000 * 1e18, + index: _i9_81 + }); + _addInitialLiquidity({ + from: _lender, + amount: 11_000 * 1e18, + index: _i9_72 + }); + _addInitialLiquidity({ + from: _lender, + amount: 25_000 * 1e18, + index: _i9_62 + }); + _addInitialLiquidity({ + from: _lender, + amount: 30_000 * 1e18, + index: _i9_52 + }); // first borrower adds collateral token and borrows - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - amount: 2 * 1e18 - } - ); - _borrow( - { - from: _borrower, - amount: 19.25 * 1e18, - indexLimit: _i9_91, - newLup: 9.917184843435912074 * 1e18 - } - ); + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + amount: 2 * 1e18 + }); + _borrow({ + from: _borrower, + amount: 19.25 * 1e18, + indexLimit: _i9_91, + newLup: 9.917184843435912074 * 1e18 + }); // second borrower adds collateral token and borrows - _pledgeCollateral( - { - from: _borrower2, - borrower: _borrower2, - amount: 1_000 * 1e18 - } - ); - _borrow( - { - from: _borrower2, - amount: 7_980 * 1e18, - indexLimit: _i9_72, - newLup: 9.721295865031779605 * 1e18 - } - ); + _pledgeCollateral({ + from: _borrower2, + borrower: _borrower2, + amount: 1_000 * 1e18 + }); + _borrow({ + from: _borrower2, + amount: 7_980 * 1e18, + indexLimit: _i9_72, + newLup: 9.721295865031779605 * 1e18 + }); /*****************************/ /*** Assert pre-kick state ***/ @@ -117,81 +99,68 @@ contract ERC20PoolLiquidationsTakeTest is ERC20HelperContract { interestRateUpdate: _startTime }) ); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 19.268509615384615394 * 1e18, - borrowerCollateral: 2 * 1e18, - borrowert0Np: 10.115967548076923081 * 1e18, - borrowerCollateralization: 1.009034539679184679 * 1e18 - } - ); - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 7_987.673076923076926760 * 1e18, - borrowerCollateral: 1_000 * 1e18, - borrowert0Np: 8.471136974495192174 * 1e18, - borrowerCollateralization: 1.217037273735858713 * 1e18 - } - ); - _assertReserveAuction( - { - reserves: 7.691586538461542154 * 1e18, - claimableReserves : 0, - claimableReservesRemaining: 0, - auctionPrice: 0, - timeRemaining: 0 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 19.268509615384615394 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 10.115967548076923081 * 1e18, + borrowerCollateralization: 1.009034539679184679 * 1e18 + }); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 7_987.673076923076926760 * 1e18, + borrowerCollateral: 1_000 * 1e18, + borrowert0Np: 8.471136974495192174 * 1e18, + borrowerCollateralization: 1.217037273735858713 * 1e18 + }); + _assertReserveAuction({ + reserves: 7.691586538461542154 * 1e18, + claimableReserves : 0, + claimableReservesRemaining: 0, + auctionPrice: 0, + timeRemaining: 0 + }); + assertEq(_quote.balanceOf(_lender), 47_000 * 1e18); // should revert if there's no auction started - _assertTakeNoAuctionRevert( - { - from: _lender, - borrower: _borrower, - maxCollateral: 10 * 1e18 - } - ); + _assertTakeNoAuctionRevert({ + from: _lender, + borrower: _borrower, + maxCollateral: 10 * 1e18 + }); } function testTakeCoolDownPeriod() external tearDown { // should revert if there's no auction started - _assertTakeNoAuctionRevert( - { - from: _lender, - borrower: _borrower, - maxCollateral: 10 * 1e18 - } - ); + _assertTakeNoAuctionRevert({ + from: _lender, + borrower: _borrower, + maxCollateral: 10 * 1e18 + }); /********************/ /*** Kick Auction ***/ /********************/ - _borrow( - { - from: _borrower2, - amount: 1_700.0 * 1e18, - indexLimit: _p9_72, - newLup: _p9_72 - } - ); + _borrow({ + from: _borrower2, + amount: 1_700.0 * 1e18, + indexLimit: _i9_72, + newLup: _p9_72 + }); skip(100 days); - _kick( - { - from: _lender, - borrower: _borrower2, - debt: 9_945.738101507554206918 * 1e18, - collateral: 1_000 * 1e18, - bond: 98.229512113654856365 * 1e18, - transferAmount: 98.229512113654856365 * 1e18 - } - ); + _kick({ + from: _lender, + borrower: _borrower2, + debt: 9_945.738101507554206918 * 1e18, + collateral: 1_000 * 1e18, + bond: 98.229512113654856365 * 1e18, + transferAmount: 98.229512113654856365 * 1e18 + }); /********************/ /*** Take Auction ***/ @@ -200,54 +169,43 @@ contract ERC20PoolLiquidationsTakeTest is ERC20HelperContract { skip(30 minutes); // should revert if still in cool down period - _assertTakeAuctionInCooldownRevert( - { - from: _lender, - borrower: _borrower2, - maxCollateral: 10 * 1e18 - } - ); + _assertTakeAuctionInCooldownRevert({ + from: _lender, + borrower: _borrower2, + maxCollateral: 10 * 1e18 + }); } function testTakeLoanColConstraintBpfPosNoResidual() external tearDown { // Increase neutralPrice so it exceeds TP - _addLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: _i1505_26, - lpAward: 10_000 * 1e27, - newLup: _p1505_26 - } - ); - - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - amount: 1_000 * 1e18 - } - ); - - _borrow( - { - from: _borrower, - amount: 9_020 * 1e18, - indexLimit: _i9_72, - newLup: _p9_72 - } - ); + _addLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: _i1505_26, + lpAward: 10_000 * 1e18, + newLup: _p1505_26 + }); + + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + amount: 1_000 * 1e18 + }); + _borrow({ + from: _borrower, + amount: 9_020 * 1e18, + indexLimit: _i9_72, + newLup: _p9_72 + }); // calling borrow stamps loan with new t0NeutralPrice - _borrow( - { - from: _borrower2, - amount: 1_700.0 * 1e18, - indexLimit: _p9_72, - newLup: _p9_72 - } - ); + _borrow({ + from: _borrower2, + amount: 1_700.0 * 1e18, + indexLimit: _i9_72, + newLup: _p9_72 + }); _assertPool( PoolParams({ @@ -266,16 +224,13 @@ contract ERC20PoolLiquidationsTakeTest is ERC20HelperContract { interestRateUpdate: block.timestamp }) ); - - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 9_689.307692307692312160* 1e18, - borrowerCollateral: 1_000 * 1e18, - borrowert0Np: 1_575.326150647652569911 * 1e18, - borrowerCollateralization: 1.003301388885552947 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 9_689.307692307692312160* 1e18, + borrowerCollateral: 1_000 * 1e18, + borrowert0Np: 1_575.326150647652569911 * 1e18, + borrowerCollateralization: 1.003301388885552947 * 1e18 + }); skip(100 days); @@ -296,8 +251,6 @@ contract ERC20PoolLiquidationsTakeTest is ERC20HelperContract { interestRateUpdate: block.timestamp - 100 days }) ); - - _assertAuction( AuctionParams({ borrower: _borrower2, @@ -314,55 +267,42 @@ contract ERC20PoolLiquidationsTakeTest is ERC20HelperContract { neutralPrice: 0 }) ); - - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 9_822.951211365485636462* 1e18, - borrowerCollateral: 1_000 * 1e18, - borrowert0Np: 1_575.326150647652569911 * 1e18, - borrowerCollateralization: 0.989651241857326201 * 1e18 - } - ); - - _kick( - { - from: _lender, - borrower: _borrower2, - debt: 9_945.738101507554206918 * 1e18, - collateral: 1_000 * 1e18, - bond: 2_946.885363409645690939 * 1e18, - transferAmount: 2_946.885363409645690939 * 1e18 - } - ); - - _assertKicker( - { - kicker: _lender, - claimable: 0, - locked: 2_946.885363409645690939 * 1e18 - } - ); - - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 9_945.738101507554206918 * 1e18, - borrowerCollateral: 1_000 * 1e18, - borrowert0Np: 1_575.326150647652569911 * 1e18, - borrowerCollateralization: 0.977433325291186371 * 1e18 - } - ); - - _assertReserveAuction( - { - reserves: 176.383108065231049467 * 1e18, - claimableReserves : 80.790723478491074900 * 1e18, - claimableReservesRemaining: 0, - auctionPrice: 0, - timeRemaining: 0 - } - ); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 9_822.951211365485636462* 1e18, + borrowerCollateral: 1_000 * 1e18, + borrowert0Np: 1_575.326150647652569911 * 1e18, + borrowerCollateralization: 0.989651241857326201 * 1e18 + }); + + _kick({ + from: _lender, + borrower: _borrower2, + debt: 9_945.738101507554206918 * 1e18, + collateral: 1_000 * 1e18, + bond: 2_946.885363409645690939 * 1e18, + transferAmount: 2_946.885363409645690939 * 1e18 + }); + + _assertKicker({ + kicker: _lender, + claimable: 0, + locked: 2_946.885363409645690939 * 1e18 + }); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 9_945.738101507554206918 * 1e18, + borrowerCollateral: 1_000 * 1e18, + borrowert0Np: 1_575.326150647652569911 * 1e18, + borrowerCollateralization: 0.977433325291186371 * 1e18 + }); + _assertReserveAuction({ + reserves: 176.383108065231049467 * 1e18, + claimableReserves : 80.790723478491074900 * 1e18, + claimableReservesRemaining: 0, + auctionPrice: 0, + timeRemaining: 0 + }); skip(47000 seconds); // 13.05 hrs @@ -383,7 +323,6 @@ contract ERC20PoolLiquidationsTakeTest is ERC20HelperContract { interestRateUpdate: block.timestamp - 47000 seconds }) ); - _assertAuction( AuctionParams({ borrower: _borrower2, @@ -400,29 +339,24 @@ contract ERC20PoolLiquidationsTakeTest is ERC20HelperContract { neutralPrice: 1_597.054445085392479852 * 1e18 }) ); - - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 9_946.405146835980073929 * 1e18, - borrowerCollateral: 1_000.000000000000000 * 1e18, - borrowert0Np: 1_575.326150647652569911 * 1e18, - borrowerCollateralization: 0.977367774740624830 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 9_946.405146835980073929 * 1e18, + borrowerCollateral: 1_000.000000000000000 * 1e18, + borrowert0Np: 1_575.326150647652569911 * 1e18, + borrowerCollateralization: 0.977367774740624830 * 1e18 + }); // BPF Positive, Loan Col constraint - _take( - { - from: _lender, - borrower: _borrower2, - maxCollateral: 1_001 * 1e18, - bondChange: 3_598.602058071496018124* 1e18, - givenAmount: 12_005.655124053999200000 * 1e18, - collateralTaken: 1000.0 * 1e18, - isReward: true - } - ); + _take({ + from: _lender, + borrower: _borrower2, + maxCollateral: 1_001 * 1e18, + bondChange: 3_598.602058071496018124* 1e18, + givenAmount: 12_005.655124053999200000 * 1e18, + collateralTaken: 1000.0 * 1e18, + isReward: true + }); // Residual is not collateralized, auction is active _assertAuction( @@ -441,58 +375,46 @@ contract ERC20PoolLiquidationsTakeTest is ERC20HelperContract { neutralPrice: 1_597.054445085392479852 * 1e18 }) ); - // Bad debt remains - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 2_235.600441131995497104 * 1e18, - borrowerCollateral: 0, - borrowert0Np: 1_575.326150647652569911 * 1e18, - borrowerCollateralization: 0 - } - ); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 2_235.600441131995497104 * 1e18, + borrowerCollateral: 0, + borrowert0Np: 1_575.326150647652569911 * 1e18, + borrowerCollateralization: 0 + }); } function testTakeCallerColConstraintBpfPosNoResidual() external tearDown { // Increase neutralPrice so it exceeds TP - _addLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: _i1505_26, - lpAward: 10_000 * 1e27, - newLup: _p1505_26 - } - ); - - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - amount: 1_000 * 1e18 - } - ); - - _borrow( - { - from: _borrower, - amount: 9_020 * 1e18, - indexLimit: _i9_72, - newLup: _p9_72 - } - ); + _addLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: _i1505_26, + lpAward: 10_000 * 1e18, + newLup: _p1505_26 + }); + + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + amount: 1_000 * 1e18 + }); + _borrow({ + from: _borrower, + amount: 9_020 * 1e18, + indexLimit: _i9_72, + newLup: _p9_72 + }); // calling borrow stamps loan with new t0NeutralPrice - _borrow( - { - from: _borrower2, - amount: 1_700.0 * 1e18, - indexLimit: _p9_72, - newLup: _p9_72 - } - ); + _borrow({ + from: _borrower2, + amount: 1_700.0 * 1e18, + indexLimit: _i9_72, + newLup: _p9_72 + }); _assertPool( PoolParams({ @@ -511,16 +433,13 @@ contract ERC20PoolLiquidationsTakeTest is ERC20HelperContract { interestRateUpdate: block.timestamp }) ); - - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 9_689.307692307692312160* 1e18, - borrowerCollateral: 1_000 * 1e18, - borrowert0Np: 1_575.326150647652569911 * 1e18, - borrowerCollateralization: 1.003301388885552947 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 9_689.307692307692312160* 1e18, + borrowerCollateral: 1_000 * 1e18, + borrowert0Np: 1_575.326150647652569911 * 1e18, + borrowerCollateralization: 1.003301388885552947 * 1e18 + }); skip(100 days); @@ -541,8 +460,6 @@ contract ERC20PoolLiquidationsTakeTest is ERC20HelperContract { interestRateUpdate: block.timestamp - 100 days }) ); - - _assertAuction( AuctionParams({ borrower: _borrower2, @@ -559,55 +476,42 @@ contract ERC20PoolLiquidationsTakeTest is ERC20HelperContract { neutralPrice: 0 }) ); - - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 9_822.951211365485636462* 1e18, - borrowerCollateral: 1_000 * 1e18, - borrowert0Np: 1_575.326150647652569911 * 1e18, - borrowerCollateralization: 0.989651241857326201 * 1e18 - } - ); - - _kick( - { - from: _lender, - borrower: _borrower2, - debt: 9_945.738101507554206918 * 1e18, - collateral: 1_000 * 1e18, - bond: 2_946.885363409645690939 * 1e18, - transferAmount: 2_946.885363409645690939 * 1e18 - } - ); - - _assertKicker( - { - kicker: _lender, - claimable: 0, - locked: 2_946.885363409645690939 * 1e18 - } - ); - - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 9_945.738101507554206918 * 1e18, - borrowerCollateral: 1_000 * 1e18, - borrowert0Np: 1_575.326150647652569911 * 1e18, - borrowerCollateralization: 0.977433325291186371 * 1e18 - } - ); - - _assertReserveAuction( - { - reserves: 176.383108065231049467 * 1e18, - claimableReserves : 80.790723478491074900 * 1e18, - claimableReservesRemaining: 0, - auctionPrice: 0, - timeRemaining: 0 - } - ); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 9_822.951211365485636462* 1e18, + borrowerCollateral: 1_000 * 1e18, + borrowert0Np: 1_575.326150647652569911 * 1e18, + borrowerCollateralization: 0.989651241857326201 * 1e18 + }); + + _kick({ + from: _lender, + borrower: _borrower2, + debt: 9_945.738101507554206918 * 1e18, + collateral: 1_000 * 1e18, + bond: 2_946.885363409645690939 * 1e18, + transferAmount: 2_946.885363409645690939 * 1e18 + }); + + _assertKicker({ + kicker: _lender, + claimable: 0, + locked: 2_946.885363409645690939 * 1e18 + }); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 9_945.738101507554206918 * 1e18, + borrowerCollateral: 1_000 * 1e18, + borrowert0Np: 1_575.326150647652569911 * 1e18, + borrowerCollateralization: 0.977433325291186371 * 1e18 + }); + _assertReserveAuction({ + reserves: 176.383108065231049467 * 1e18, + claimableReserves : 80.790723478491074900 * 1e18, + claimableReservesRemaining: 0, + auctionPrice: 0, + timeRemaining: 0 + }); skip(43000 seconds); // 11.94 hrs @@ -628,7 +532,6 @@ contract ERC20PoolLiquidationsTakeTest is ERC20HelperContract { interestRateUpdate: block.timestamp - 43000 seconds }) ); - _assertAuction( AuctionParams({ borrower: _borrower2, @@ -645,29 +548,24 @@ contract ERC20PoolLiquidationsTakeTest is ERC20HelperContract { neutralPrice: 1_597.054445085392479852 * 1e18 }) ); - - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 9_946.348375279124882460 * 1e18, - borrowerCollateral: 1_000.000000000000000 * 1e18, - borrowert0Np: 1_575.326150647652569911 * 1e18, - borrowerCollateralization: 0.977373353339734632 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 9_946.348375279124882460 * 1e18, + borrowerCollateral: 1_000.000000000000000 * 1e18, + borrowert0Np: 1_575.326150647652569911 * 1e18, + borrowerCollateralization: 0.977373353339734632 * 1e18 + }); // BPF Positive, caller collateral is constraint - _take( - { - from: _lender, - borrower: _borrower2, - maxCollateral: 10 * 1e18, - bondChange: 77.051043093949465946 * 1e18, - givenAmount: 259.336494770337503360 * 1e18, - collateralTaken: 10.0 * 1e18, - isReward: true - } - ); + _take({ + from: _lender, + borrower: _borrower2, + maxCollateral: 10 * 1e18, + bondChange: 77.051043093949465946 * 1e18, + givenAmount: 259.336494770337503360 * 1e18, + collateralTaken: 10.0 * 1e18, + isReward: true + }); // Residual is not collateralized, auction is active _assertAuction( @@ -686,57 +584,45 @@ contract ERC20PoolLiquidationsTakeTest is ERC20HelperContract { neutralPrice: 1_597.054445085392479852 * 1e18 }) ); - - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 10_460.307309872275586822 * 1e18, - borrowerCollateral: 990 * 1e18, - borrowert0Np: 1_575.326150647652569911 * 1e18, - borrowerCollateralization: 0.920057377023560467 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 10_460.307309872275586822 * 1e18, + borrowerCollateral: 990 * 1e18, + borrowert0Np: 1_575.326150647652569911 * 1e18, + borrowerCollateralization: 0.920057377023560467 * 1e18 + }); } function testTakeCallerColConstraintBpfPosResidual () external tearDown { // Increase neutralPrice so it exceeds TP - _addLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: _i1505_26, - lpAward: 10_000 * 1e27, - newLup: _p1505_26 - } - ); - - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - amount: 1_000 * 1e18 - } - ); - - _borrow( - { - from: _borrower, - amount: 9_020 * 1e18, - indexLimit: _i9_72, - newLup: _p9_72 - } - ); + _addLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: _i1505_26, + lpAward: 10_000 * 1e18, + newLup: _p1505_26 + }); + + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + amount: 1_000 * 1e18 + }); + _borrow({ + from: _borrower, + amount: 9_020 * 1e18, + indexLimit: _i9_72, + newLup: _p9_72 + }); // calling borrow stamps loan with new t0NeutralPrice - _borrow( - { - from: _borrower2, - amount: 1_700.0 * 1e18, - indexLimit: _p9_72, - newLup: _p9_72 - } - ); + _borrow({ + from: _borrower2, + amount: 1_700.0 * 1e18, + indexLimit: _i9_72, + newLup: _p9_72 + }); _assertPool( PoolParams({ @@ -755,16 +641,13 @@ contract ERC20PoolLiquidationsTakeTest is ERC20HelperContract { interestRateUpdate: block.timestamp }) ); - - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 9_689.307692307692312160* 1e18, - borrowerCollateral: 1_000 * 1e18, - borrowert0Np: 1_575.326150647652569911 * 1e18, - borrowerCollateralization: 1.003301388885552947 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 9_689.307692307692312160* 1e18, + borrowerCollateral: 1_000 * 1e18, + borrowert0Np: 1_575.326150647652569911 * 1e18, + borrowerCollateralization: 1.003301388885552947 * 1e18 + }); skip(100 days); @@ -785,8 +668,6 @@ contract ERC20PoolLiquidationsTakeTest is ERC20HelperContract { interestRateUpdate: block.timestamp - 100 days }) ); - - _assertAuction( AuctionParams({ borrower: _borrower2, @@ -803,55 +684,42 @@ contract ERC20PoolLiquidationsTakeTest is ERC20HelperContract { neutralPrice: 0 }) ); - - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 9_822.951211365485636462* 1e18, - borrowerCollateral: 1_000 * 1e18, - borrowert0Np: 1_575.326150647652569911 * 1e18, - borrowerCollateralization: 0.989651241857326201 * 1e18 - } - ); - - _kick( - { - from: _lender, - borrower: _borrower2, - debt: 9_945.738101507554206918 * 1e18, - collateral: 1_000 * 1e18, - bond: 2_946.885363409645690939 * 1e18, - transferAmount: 2_946.885363409645690939 * 1e18 - } - ); - - _assertKicker( - { - kicker: _lender, - claimable: 0, - locked: 2_946.885363409645690939 * 1e18 - } - ); - - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 9_945.738101507554206918 * 1e18, - borrowerCollateral: 1_000 * 1e18, - borrowert0Np: 1_575.326150647652569911 * 1e18, - borrowerCollateralization: 0.977433325291186371 * 1e18 - } - ); - - _assertReserveAuction( - { - reserves: 176.383108065231049467 * 1e18, - claimableReserves : 80.790723478491074900 * 1e18, - claimableReservesRemaining: 0, - auctionPrice: 0, - timeRemaining: 0 - } - ); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 9_822.951211365485636462* 1e18, + borrowerCollateral: 1_000 * 1e18, + borrowert0Np: 1_575.326150647652569911 * 1e18, + borrowerCollateralization: 0.989651241857326201 * 1e18 + }); + + _kick({ + from: _lender, + borrower: _borrower2, + debt: 9_945.738101507554206918 * 1e18, + collateral: 1_000 * 1e18, + bond: 2_946.885363409645690939 * 1e18, + transferAmount: 2_946.885363409645690939 * 1e18 + }); + + _assertKicker({ + kicker: _lender, + claimable: 0, + locked: 2_946.885363409645690939 * 1e18 + }); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 9_945.738101507554206918 * 1e18, + borrowerCollateral: 1_000 * 1e18, + borrowert0Np: 1_575.326150647652569911 * 1e18, + borrowerCollateralization: 0.977433325291186371 * 1e18 + }); + _assertReserveAuction({ + reserves: 176.383108065231049467 * 1e18, + claimableReserves : 80.790723478491074900 * 1e18, + claimableReservesRemaining: 0, + auctionPrice: 0, + timeRemaining: 0 + }); skip(43000 seconds); // 11.94 hrs @@ -872,7 +740,6 @@ contract ERC20PoolLiquidationsTakeTest is ERC20HelperContract { interestRateUpdate: block.timestamp - 43000 seconds }) ); - _assertAuction( AuctionParams({ borrower: _borrower2, @@ -889,29 +756,24 @@ contract ERC20PoolLiquidationsTakeTest is ERC20HelperContract { neutralPrice: 1_597.054445085392479852 * 1e18 }) ); - - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 9_946.348375279124882460 * 1e18, - borrowerCollateral: 1_000.000000000000000 * 1e18, - borrowert0Np: 1_575.326150647652569911 * 1e18, - borrowerCollateralization: 0.977373353339734632 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 9_946.348375279124882460 * 1e18, + borrowerCollateral: 1_000.000000000000000 * 1e18, + borrowert0Np: 1_575.326150647652569911 * 1e18, + borrowerCollateralization: 0.977373353339734632 * 1e18 + }); // BPF Positive, Caller Col constraint - _take( - { - from: _lender, - borrower: _borrower2, - maxCollateral: 577.0 * 1e18, - bondChange: 4_445.845186520884185062 * 1e18, - givenAmount: 14_963.715748248473943872 * 1e18, - collateralTaken: 577.000000000000000000 * 1e18, - isReward: true - } - ); + _take({ + from: _lender, + borrower: _borrower2, + maxCollateral: 577.0 * 1e18, + bondChange: 4_445.845186520884185062 * 1e18, + givenAmount: 14_963.715748248473943872 * 1e18, + collateralTaken: 577.000000000000000000 * 1e18, + isReward: true + }); // Residual is collateralized, auction is not active _assertAuction( @@ -930,29 +792,24 @@ contract ERC20PoolLiquidationsTakeTest is ERC20HelperContract { neutralPrice: 0 }) ); - - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 124.722199821073865675 * 1e18, - borrowerCollateral: 423.000000000000000000 * 1e18, - borrowert0Np: 0.303909165615512483 * 1e18, - borrowerCollateralization: 5_105.158167959369510853 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 124.722199821073865675 * 1e18, + borrowerCollateral: 423.000000000000000000 * 1e18, + borrowert0Np: 0.303909165615512483 * 1e18, + borrowerCollateralization: 5_105.158167959369510853 * 1e18 + }); } function testTakeCallerColConstraintBpfNegResidual () external tearDown { - _borrow( - { - from: _borrower2, - amount: 1_700.0 * 1e18, - indexLimit: _p9_72, - newLup: _p9_72 - } - ); + _borrow({ + from: _borrower2, + amount: 1_700.0 * 1e18, + indexLimit: _i9_72, + newLup: _p9_72 + }); _assertPool( PoolParams({ @@ -971,29 +828,24 @@ contract ERC20PoolLiquidationsTakeTest is ERC20HelperContract { interestRateUpdate: block.timestamp }) ); - - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 9_689.307692307692312160 * 1e18, - borrowerCollateral: 1_000 * 1e18, - borrowert0Np: 10.275765152019230606 * 1e18, - borrowerCollateralization: 1.003301388885552947 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 9_689.307692307692312160 * 1e18, + borrowerCollateral: 1_000 * 1e18, + borrowert0Np: 10.275765152019230606 * 1e18, + borrowerCollateralization: 1.003301388885552947 * 1e18 + }); skip(100 days); - _kick( - { - from: _lender, - borrower: _borrower2, - debt: 9_945.738101507554206918 * 1e18, - collateral: 1_000 * 1e18, - bond: 98.229512113654856365 * 1e18, - transferAmount: 98.229512113654856365 * 1e18 - } - ); + _kick({ + from: _lender, + borrower: _borrower2, + debt: 9_945.738101507554206918 * 1e18, + collateral: 1_000 * 1e18, + bond: 98.229512113654856365 * 1e18, + transferAmount: 98.229512113654856365 * 1e18 + }); _assertAuction( AuctionParams({ @@ -1011,31 +863,25 @@ contract ERC20PoolLiquidationsTakeTest is ERC20HelperContract { neutralPrice: 10.417497612122395691 * 1e18 }) ); - _assertKicker( - { - kicker: _lender, - claimable: 0, - locked: 98.229512113654856365 * 1e18 - } - ); - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 9_945.738101507554206918 * 1e18, - borrowerCollateral: 1_000 * 1e18, - borrowert0Np: 10.275765152019230606 * 1e18, - borrowerCollateralization: 0.977433325291186371 * 1e18 - } - ); - _assertReserveAuction( - { - reserves: 147.625795655539437491 * 1e18, - claimableReserves : 97.799433758115930094 * 1e18, - claimableReservesRemaining: 0, - auctionPrice: 0, - timeRemaining: 0 - } - ); + _assertKicker({ + kicker: _lender, + claimable: 0, + locked: 98.229512113654856365 * 1e18 + }); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 9_945.738101507554206918 * 1e18, + borrowerCollateral: 1_000 * 1e18, + borrowert0Np: 10.275765152019230606 * 1e18, + borrowerCollateralization: 0.977433325291186371 * 1e18 + }); + _assertReserveAuction({ + reserves: 147.625795655539437491 * 1e18, + claimableReserves : 97.799433758115930094 * 1e18, + claimableReservesRemaining: 0, + auctionPrice: 0, + timeRemaining: 0 + }); skip(2 hours); @@ -1056,7 +902,6 @@ contract ERC20PoolLiquidationsTakeTest is ERC20HelperContract { interestRateUpdate: block.timestamp - 2 hours }) ); - _assertAuction( AuctionParams({ borrower: _borrower2, @@ -1073,29 +918,24 @@ contract ERC20PoolLiquidationsTakeTest is ERC20HelperContract { neutralPrice: 10.417497612122395691 * 1e18 }) ); - - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 9_945.840284273233679079 * 1e18, - borrowerCollateral: 1_000.000000000000000 * 1e18, - borrowert0Np: 10.275765152019230606 * 1e18, - borrowerCollateralization: 0.977423283219567398 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 9_945.840284273233679079 * 1e18, + borrowerCollateral: 1_000.000000000000000 * 1e18, + borrowert0Np: 10.275765152019230606 * 1e18, + borrowerCollateralization: 0.977423283219567398 * 1e18 + }); // BPF Negative, Caller collateral constraint - _take( - { - from: _lender, - borrower: _borrower2, - maxCollateral: 10.0 * 1e18, - bondChange: 16.667996179395833107 * 1e18, - givenAmount: 1_666.799617939583310720 * 1e18, - collateralTaken: 10.0 * 1e18, - isReward: false - } - ); + _take({ + from: _lender, + borrower: _borrower2, + maxCollateral: 10.0 * 1e18, + bondChange: 16.667996179395833107 * 1e18, + givenAmount: 1_666.799617939583310720 * 1e18, + collateralTaken: 10.0 * 1e18, + isReward: false + }); _assertPool( PoolParams({ @@ -1114,7 +954,6 @@ contract ERC20PoolLiquidationsTakeTest is ERC20HelperContract { interestRateUpdate: block.timestamp - 2 hours }) ); - // Residual is collateralized, auction is not active _assertAuction( AuctionParams({ @@ -1132,58 +971,46 @@ contract ERC20PoolLiquidationsTakeTest is ERC20HelperContract { neutralPrice: 0 }) ); - - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 8_975.249486232776725894 * 1e18, - borrowerCollateral: 990.000000000000000000 * 1e18, - borrowert0Np: 9.438566662973887791 * 1e18, - borrowerCollateralization: 1.072291407736791833 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 8_975.249486232776725894 * 1e18, + borrowerCollateral: 990.000000000000000000 * 1e18, + borrowert0Np: 9.438566662973887791 * 1e18, + borrowerCollateralization: 1.072291407736791833 * 1e18 + }); } function testTakeLoanDebtConstraintBpfPosResidual() external tearDown { // Increase neutralPrice so it exceeds TP - _addLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: _i1505_26, - lpAward: 10_000 * 1e27, - newLup: _p1505_26 - } - ); - - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - amount: 1_000 * 1e18 - } - ); - - _borrow( - { - from: _borrower, - amount: 9_020 * 1e18, - indexLimit: _i9_72, - newLup: _p9_72 - } - ); + _addLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: _i1505_26, + lpAward: 10_000 * 1e18, + newLup: _p1505_26 + }); + + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + amount: 1_000 * 1e18 + }); + _borrow({ + from: _borrower, + amount: 9_020 * 1e18, + indexLimit: _i9_72, + newLup: _p9_72 + }); // calling borrow stamps loan with new t0NeutralPrice - _borrow( - { - from: _borrower2, - amount: 1_700.0 * 1e18, - indexLimit: _p9_72, - newLup: _p9_72 - } - ); + _borrow({ + from: _borrower2, + amount: 1_700.0 * 1e18, + indexLimit: _i9_72, + newLup: _p9_72 + }); _assertPool( PoolParams({ @@ -1202,16 +1029,13 @@ contract ERC20PoolLiquidationsTakeTest is ERC20HelperContract { interestRateUpdate: block.timestamp }) ); - - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 9_689.307692307692312160* 1e18, - borrowerCollateral: 1_000 * 1e18, - borrowert0Np: 1_575.326150647652569911 * 1e18, - borrowerCollateralization: 1.003301388885552947 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 9_689.307692307692312160* 1e18, + borrowerCollateral: 1_000 * 1e18, + borrowert0Np: 1_575.326150647652569911 * 1e18, + borrowerCollateralization: 1.003301388885552947 * 1e18 + }); skip(100 days); @@ -1232,8 +1056,6 @@ contract ERC20PoolLiquidationsTakeTest is ERC20HelperContract { interestRateUpdate: block.timestamp - 100 days }) ); - - _assertAuction( AuctionParams({ borrower: _borrower2, @@ -1250,55 +1072,42 @@ contract ERC20PoolLiquidationsTakeTest is ERC20HelperContract { neutralPrice: 0 }) ); - - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 9_822.951211365485636462* 1e18, - borrowerCollateral: 1_000 * 1e18, - borrowert0Np: 1_575.326150647652569911 * 1e18, - borrowerCollateralization: 0.989651241857326201 * 1e18 - } - ); - - _kick( - { - from: _lender, - borrower: _borrower2, - debt: 9_945.738101507554206918 * 1e18, - collateral: 1_000 * 1e18, - bond: 2_946.885363409645690939 * 1e18, - transferAmount: 2_946.885363409645690939 * 1e18 - } - ); - - _assertKicker( - { - kicker: _lender, - claimable: 0, - locked: 2_946.885363409645690939 * 1e18 - } - ); - - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 9_945.738101507554206918 * 1e18, - borrowerCollateral: 1_000 * 1e18, - borrowert0Np: 1_575.326150647652569911 * 1e18, - borrowerCollateralization: 0.977433325291186371 * 1e18 - } - ); - - _assertReserveAuction( - { - reserves: 176.383108065231049467 * 1e18, - claimableReserves : 80.790723478491074900 * 1e18, - claimableReservesRemaining: 0, - auctionPrice: 0, - timeRemaining: 0 - } - ); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 9_822.951211365485636462* 1e18, + borrowerCollateral: 1_000 * 1e18, + borrowert0Np: 1_575.326150647652569911 * 1e18, + borrowerCollateralization: 0.989651241857326201 * 1e18 + }); + + _kick({ + from: _lender, + borrower: _borrower2, + debt: 9_945.738101507554206918 * 1e18, + collateral: 1_000 * 1e18, + bond: 2_946.885363409645690939 * 1e18, + transferAmount: 2_946.885363409645690939 * 1e18 + }); + + _assertKicker({ + kicker: _lender, + claimable: 0, + locked: 2_946.885363409645690939 * 1e18 + }); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 9_945.738101507554206918 * 1e18, + borrowerCollateral: 1_000 * 1e18, + borrowert0Np: 1_575.326150647652569911 * 1e18, + borrowerCollateralization: 0.977433325291186371 * 1e18 + }); + _assertReserveAuction({ + reserves: 176.383108065231049467 * 1e18, + claimableReserves : 80.790723478491074900 * 1e18, + claimableReservesRemaining: 0, + auctionPrice: 0, + timeRemaining: 0 + }); skip(43000 seconds); // 11.94 hrs @@ -1319,7 +1128,6 @@ contract ERC20PoolLiquidationsTakeTest is ERC20HelperContract { interestRateUpdate: block.timestamp - 43000 seconds }) ); - _assertAuction( AuctionParams({ borrower: _borrower2, @@ -1336,29 +1144,24 @@ contract ERC20PoolLiquidationsTakeTest is ERC20HelperContract { neutralPrice: 1_597.054445085392479852 * 1e18 }) ); - - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 9_946.348375279124882460 * 1e18, - borrowerCollateral: 1_000.000000000000000 * 1e18, - borrowert0Np: 1_575.326150647652569911 * 1e18, - borrowerCollateralization: 0.977373353339734632 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 9_946.348375279124882460 * 1e18, + borrowerCollateral: 1_000.000000000000000 * 1e18, + borrowert0Np: 1_575.326150647652569911 * 1e18, + borrowerCollateralization: 0.977373353339734632 * 1e18 + }); // BPF Positive, Loan Debt constraint - _take( - { - from: _lender, - borrower: _borrower2, - maxCollateral: 1_001 * 1e18, - bondChange: 4_498.564564314381167419 * 1e18, - givenAmount: 15_141.157325863044791651 * 1e18, - collateralTaken: 583.842136806534270091 * 1e18, - isReward: true - } - ); + _take({ + from: _lender, + borrower: _borrower2, + maxCollateral: 1_001 * 1e18, + bondChange: 4_498.564564314381167419 * 1e18, + givenAmount: 15_141.157325863044791651 * 1e18, + collateralTaken: 583.842136806534270091 * 1e18, + isReward: true + }); // Residual is collateralized, auction is not active _assertAuction( @@ -1377,51 +1180,45 @@ contract ERC20PoolLiquidationsTakeTest is ERC20HelperContract { neutralPrice: 0 }) ); - - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 0, - borrowerCollateral: 416.157863193465729909 * 1e18, - borrowert0Np: 0, - borrowerCollateralization: 1 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 0, + borrowerCollateral: 416.157863193465729909 * 1e18, + borrowert0Np: 0, + borrowerCollateralization: 1 * 1e18 + }); } function testTakeAndSettle() external tearDown { // Borrower2 borrows - _borrow( - { - from: _borrower2, - amount: 1_730 * 1e18, - indexLimit: _i9_72, - newLup: 9.721295865031779605 * 1e18 - } - ); + _borrow({ + from: _borrower2, + amount: 1_730 * 1e18, + indexLimit: _i9_72, + newLup: 9.721295865031779605 * 1e18 + }); // Skip to make borrower undercollateralized skip(100 days); - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 9_853.394241979221645666 * 1e18, - borrowerCollateral: 1_000 * 1e18, - borrowert0Np: 10.307611531622595991 * 1e18, - borrowerCollateralization: 0.986593617011217057 * 1e18 - } - ); - _kick( - { - from: _lender, - borrower: _borrower2, - debt: 9_976.561670003961916237 * 1e18, - collateral: 1_000 * 1e18, - bond: 98.533942419792216457 * 1e18, - transferAmount: 98.533942419792216457 * 1e18 - } - ); + + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 9_853.394241979221645666 * 1e18, + borrowerCollateral: 1_000 * 1e18, + borrowert0Np: 10.307611531622595991 * 1e18, + borrowerCollateralization: 0.986593617011217057 * 1e18 + }); + + _kick({ + from: _lender, + borrower: _borrower2, + debt: 9_976.561670003961916237 * 1e18, + collateral: 1_000 * 1e18, + bond: 98.533942419792216457 * 1e18, + transferAmount: 98.533942419792216457 * 1e18 + }); + _assertAuction( AuctionParams({ borrower: _borrower2, @@ -1438,33 +1235,28 @@ contract ERC20PoolLiquidationsTakeTest is ERC20HelperContract { neutralPrice: 10.449783245217816340 * 1e18 }) ); - _assertKicker( - { - kicker: _lender, - claimable: 0, - locked: 98.533942419792216457 * 1e18 - } - ); - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 9_976.561670003961916237 * 1e18, - borrowerCollateral: 1_000 * 1e18, - borrowert0Np: 10.307611531622595991 * 1e18, - borrowerCollateralization: 0.974413448899967463 * 1e18 - } - ); - _assertReserveAuction( - { - reserves: 148.064352861909228810 * 1e18, - claimableReserves : 98.083873122003682866 * 1e18, - claimableReservesRemaining: 0, - auctionPrice: 0, - timeRemaining: 0 - } - ); + _assertKicker({ + kicker: _lender, + claimable: 0, + locked: 98.533942419792216457 * 1e18 + }); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 9_976.561670003961916237 * 1e18, + borrowerCollateral: 1_000 * 1e18, + borrowert0Np: 10.307611531622595991 * 1e18, + borrowerCollateralization: 0.974413448899967463 * 1e18 + }); + _assertReserveAuction({ + reserves: 148.064352861909228810 * 1e18, + claimableReserves : 98.083873122003682866 * 1e18, + claimableReservesRemaining: 0, + auctionPrice: 0, + timeRemaining: 0 + }); uint256 preTakeSnapshot = vm.snapshot(); + skip(364 minutes); _assertAuction( @@ -1483,38 +1275,23 @@ contract ERC20PoolLiquidationsTakeTest is ERC20HelperContract { neutralPrice: 10.449783245217816340 * 1e18 }) ); - - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 9_976.872588243234769567 * 1e18, - borrowerCollateral: 1_000 * 1e18, - borrowert0Np: 10.307611531622595991 * 1e18, - borrowerCollateralization: 0.974383082378677738 * 1e18 - } - ); - - _take( - { - from: _lender, - borrower: _borrower2, - maxCollateral: 1_000 * 1e18, - bondChange: 99.778877943799773760 * 1e18, - givenAmount: 9_977.887794379977376000 * 1e18, - collateralTaken: 1000.0 * 1e18, - isReward: true - } - ); - - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 797.144752984083601436 * 1e18, - borrowerCollateral: 0, - borrowert0Np: 10.307611531622595991 * 1e18, - borrowerCollateralization: 0 - } - ); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 9_976.872588243234769567 * 1e18, + borrowerCollateral: 1_000 * 1e18, + borrowert0Np: 10.307611531622595991 * 1e18, + borrowerCollateralization: 0.974383082378677738 * 1e18 + }); + + _take({ + from: _lender, + borrower: _borrower2, + maxCollateral: 1_000 * 1e18, + bondChange: 99.778877943799773760 * 1e18, + givenAmount: 9_977.887794379977376000 * 1e18, + collateralTaken: 1000.0 * 1e18, + isReward: true + }); _assertAuction( AuctionParams({ @@ -1532,14 +1309,18 @@ contract ERC20PoolLiquidationsTakeTest is ERC20HelperContract { neutralPrice: 10.449783245217816340 * 1e18 }) ); - - _assertKicker( - { - kicker: _lender, - claimable: 0, - locked: 198.312820363591990217 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 797.144752984083601436 * 1e18, + borrowerCollateral: 0, + borrowert0Np: 10.307611531622595991 * 1e18, + borrowerCollateralization: 0 + }); + _assertKicker({ + kicker: _lender, + claimable: 0, + locked: 198.312820363591990217 * 1e18 + }); vm.revertTo(preTakeSnapshot); @@ -1562,30 +1343,26 @@ contract ERC20PoolLiquidationsTakeTest is ERC20HelperContract { neutralPrice: 10.449783245217816340 * 1e18 }) ); - - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 9_977.074177773911990381 * 1e18, - borrowerCollateral: 1_000 * 1e18, - borrowert0Np: 10.307611531622595991 * 1e18, - borrowerCollateralization: 0.974363394700228467 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 9_977.074177773911990381 * 1e18, + borrowerCollateral: 1_000 * 1e18, + borrowert0Np: 10.307611531622595991 * 1e18, + borrowerCollateralization: 0.974363394700228467 * 1e18 + }); // partial take for 20 collateral // Collateral amount is restrained by taker - _take( - { - from: _lender, - borrower: _borrower2, - maxCollateral: 20 * 1e18, - bondChange: 0.130622290565222707 * 1e18, - givenAmount: 13.062229056522270720 * 1e18, - collateralTaken: 20 * 1e18, - isReward: true - } - ); + _take({ + from: _lender, + borrower: _borrower2, + maxCollateral: 20 * 1e18, + bondChange: 0.130622290565222707 * 1e18, + givenAmount: 13.062229056522270720 * 1e18, + collateralTaken: 20 * 1e18, + isReward: true + }); + _assertAuction( AuctionParams({ borrower: _borrower2, @@ -1602,46 +1379,38 @@ contract ERC20PoolLiquidationsTakeTest is ERC20HelperContract { neutralPrice: 10.449783245217816340 * 1e18 }) ); - _assertKicker( - { - kicker: _lender, - claimable: 0, - locked: 98.664564710357439164 * 1e18 // locked bond + reward, auction is not yet finished - } - ); - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 10_662.537763452128781688 * 1e18, - borrowerCollateral: 980 * 1e18, - borrowert0Np: 10.307611531622595991 * 1e18, - borrowerCollateralization: 0.893489913853932440 * 1e18 - } - ); - + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 10_662.537763452128781688 * 1e18, + borrowerCollateral: 980 * 1e18, + borrowert0Np: 10.307611531622595991 * 1e18, + borrowerCollateralization: 0.893489913853932440 * 1e18 + }); + _assertKicker({ + kicker: _lender, + claimable: 0, + locked: 98.664564710357439164 * 1e18 // locked bond + reward, auction is not yet finished + }); // reserves should increase after take action - _assertReserveAuction( - { - reserves: 846.536571996419330152 * 1e18, - claimableReserves : 793.126206771778158186 * 1e18, - claimableReservesRemaining: 0, - auctionPrice: 0, - timeRemaining: 0 - } - ); + _assertReserveAuction({ + reserves: 846.536571996419330152 * 1e18, + claimableReserves : 793.126206771778158186 * 1e18, + claimableReservesRemaining: 0, + auctionPrice: 0, + timeRemaining: 0 + }); // take remaining collateral - _take( - { - from: _lender, - borrower: _borrower2, - maxCollateral: 981 * 1e18, - bondChange: 6.400492237695912653 * 1e18, - givenAmount: 640.049223769591265280 * 1e18, - collateralTaken: 980 * 1e18, - isReward: true - } - ); + _take({ + from: _lender, + borrower: _borrower2, + maxCollateral: 981 * 1e18, + bondChange: 6.400492237695912653 * 1e18, + givenAmount: 640.049223769591265280 * 1e18, + collateralTaken: 980 * 1e18, + isReward: true + }); + _assertAuction( AuctionParams({ borrower: _borrower2, @@ -1658,62 +1427,51 @@ contract ERC20PoolLiquidationsTakeTest is ERC20HelperContract { neutralPrice: 10.449783245217816340 * 1e18 }) ); - _assertKicker( - { - kicker: _lender, - claimable: 0, - locked: 105.065056948053351817 * 1e18 // locked bond + reward, auction is not yet finalized - } - ); - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 10_028.889031920233428707 * 1e18, - borrowerCollateral: 0, - borrowert0Np: 10.307611531622595991 * 1e18, - borrowerCollateralization: 0 - } - ); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 10_028.889031920233428707 * 1e18, + borrowerCollateral: 0, + borrowert0Np: 10.307611531622595991 * 1e18, + borrowerCollateralization: 0 + }); + _assertKicker({ + kicker: _lender, + claimable: 0, + locked: 105.065056948053351817 * 1e18 // locked bond + reward, auction is not yet finalized + }); // reserves should increase after take action - _assertReserveAuction( - { - reserves: 846.536571996419329799 * 1e18, - claimableReserves : 796.294450429437634598 * 1e18, - claimableReservesRemaining: 0, - auctionPrice: 0, - timeRemaining: 0 - } - ); + _assertReserveAuction({ + reserves: 846.536571996419329799 * 1e18, + claimableReserves : 796.294450429437634598 * 1e18, + claimableReservesRemaining: 0, + auctionPrice: 0, + timeRemaining: 0 + }); // should revert if there's no more collateral to be auctioned - _assertTakeInsufficentCollateralRevert( - { - from: _lender, - borrower: _borrower2, - maxCollateral: 10 * 1e18 - } - ); + _assertTakeInsufficentCollateralRevert({ + from: _lender, + borrower: _borrower2, + maxCollateral: 10 * 1e18 + }); // full clear / debt settle uint256 postTakeSnapshot = vm.snapshot(); - _assertBucket( - { - index: 3_696, - lpBalance: 2_000 * 1e27, - collateral: 0, - deposit: 2_118.911507166546112000 * 1e18, - exchangeRate: 1.059455753583273056000000000 * 1e27 - } - ); - _settle( - { - from: _lender, - borrower: _borrower2, - maxDepth: 10, - settledDebt: 9_891.935520844277346922 * 1e18 - } - ); + _assertBucket({ + index: 3_696, + lpBalance: 2_000 * 1e18, + collateral: 0, + deposit: 2_118.911507166546112000 * 1e18, + exchangeRate: 1.059455753583273056 * 1e18 + }); + + _settle({ + from: _lender, + borrower: _borrower2, + maxDepth: 10, + settledDebt: 9_891.935520844277346922 * 1e18 + }); _assertAuction( AuctionParams({ @@ -1731,137 +1489,109 @@ contract ERC20PoolLiquidationsTakeTest is ERC20HelperContract { neutralPrice: 0 }) ); - _assertKicker( - { - kicker: _lender, - claimable: 105.065056948053351817 * 1e18, - locked: 0 - } - ); - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 0, - borrowerCollateral: 0, - borrowert0Np: 10.307611531622595991 * 1e18, - borrowerCollateralization: 1 * 1e18 - } - ); - - _assertBucket( - { - index: _i9_91, - lpBalance: 0, // bucket is bankrupt - collateral: 0, - deposit: 0, - exchangeRate: 1 * 1e27 - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: _i9_91, - lpBalance: 0, // bucket is bankrupt - depositTime: _startTime - } - ); - _assertBucket( - { - index: _i9_81, - lpBalance: 0, // bucket is bankrupt - collateral: 0, - deposit: 0, - exchangeRate: 1 * 1e27 - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: _i9_81, - lpBalance: 0, // bucket is bankrupt - depositTime: _startTime - } - ); - _assertBucket( - { - index: _i9_72, - lpBalance: 11_000 * 1e27, - collateral: 0, - deposit: 8_935.875749431291350857 * 1e18, - exchangeRate: 0.812352340857390122805181818 * 1e27 - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: _i9_72, - lpBalance: 11_000 * 1e27, - depositTime: _startTime - } - ); - _assertBucket( - { - index: _i9_62, - lpBalance: 25_000 * 1e27, - collateral: 0, - deposit: 25_000 * 1e18, - exchangeRate: 1 * 1e27 - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: _i9_62, - lpBalance: 25_000 * 1e27, - depositTime: _startTime - } - ); - _assertBucket( - { - index: _i9_52, - lpBalance: 30_000 * 1e27, - collateral: 0, - deposit: 30_000 * 1e18, - exchangeRate: 1 * 1e27 - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: _i9_52, - lpBalance: 30_000 * 1e27, - depositTime: _startTime - } - ); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 0, + borrowerCollateral: 0, + borrowert0Np: 10.307611531622595991 * 1e18, + borrowerCollateralization: 1 * 1e18 + }); + _assertKicker({ + kicker: _lender, + claimable: 105.065056948053351817 * 1e18, + locked: 0 + }); + _assertBucket({ + index: _i9_91, + lpBalance: 0, // bucket is bankrupt + collateral: 0, + deposit: 0, + exchangeRate: 1 * 1e18 + }); + _assertLenderLpBalance({ + lender: _lender, + index: _i9_91, + lpBalance: 0, // bucket is bankrupt + depositTime: _startTime + }); + _assertBucket({ + index: _i9_81, + lpBalance: 0, // bucket is bankrupt + collateral: 0, + deposit: 0, + exchangeRate: 1 * 1e18 + }); + _assertLenderLpBalance({ + lender: _lender, + index: _i9_81, + lpBalance: 0, // bucket is bankrupt + depositTime: _startTime + }); + _assertBucket({ + index: _i9_72, + lpBalance: 11_000 * 1e18, + collateral: 0, + deposit: 8_935.875749431291350857 * 1e18, + exchangeRate: 0.812352340857390123 * 1e18 + }); + _assertLenderLpBalance({ + lender: _lender, + index: _i9_72, + lpBalance: 11_000 * 1e18, + depositTime: _startTime + }); + _assertBucket({ + index: _i9_62, + lpBalance: 25_000 * 1e18, + collateral: 0, + deposit: 25_000 * 1e18, + exchangeRate: 1 * 1e18 + }); + _assertLenderLpBalance({ + lender: _lender, + index: _i9_62, + lpBalance: 25_000 * 1e18, + depositTime: _startTime + }); + _assertBucket({ + index: _i9_52, + lpBalance: 30_000 * 1e18, + collateral: 0, + deposit: 30_000 * 1e18, + exchangeRate: 1 * 1e18 + }); + _assertLenderLpBalance({ + lender: _lender, + index: _i9_52, + lpBalance: 30_000 * 1e18, + depositTime: _startTime + }); vm.revertTo(postTakeSnapshot); - _assertReserveAuction( - { - reserves: 846.536571996419329799 * 1e18, - claimableReserves : 796.294450429437634598 * 1e18, - claimableReservesRemaining: 0, - auctionPrice: 0, - timeRemaining: 0 - } - ); + + _assertReserveAuction({ + reserves: 846.536571996419329799 * 1e18, + claimableReserves : 796.294450429437634598 * 1e18, + claimableReservesRemaining: 0, + auctionPrice: 0, + timeRemaining: 0 + }); + // partial clears / debt settled - max buckets to use is 1, remaining will be taken from reserves - _settle( - { - from: _lender, - borrower: _borrower2, - maxDepth: 1, - settledDebt: 2_923.975862386543877283 * 1e18 - } - ); - _assertReserveAuction( - { - reserves: 0.989870342666661239 * 1e18, - claimableReserves : 0, - claimableReservesRemaining: 0, - auctionPrice: 0, - timeRemaining: 0 - } - ); + _settle({ + from: _lender, + borrower: _borrower2, + maxDepth: 1, + settledDebt: 2_923.975862386543877283 * 1e18 + }); + + _assertReserveAuction({ + reserves: 0.989870342666661239 * 1e18, + claimableReserves : 0, + claimableReservesRemaining: 0, + auctionPrice: 0, + timeRemaining: 0 + }); _assertAuction( AuctionParams({ borrower: _borrower2, @@ -1878,31 +1608,27 @@ contract ERC20PoolLiquidationsTakeTest is ERC20HelperContract { neutralPrice: 10.449783245217816340 * 1e18 }) ); - _assertKicker( - { - kicker: _lender, - claimable: 0, - locked: 105.065056948053351817 * 1e18 // locked bond + reward, auction is not yet finalized - } - ); - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 7_064.430823099934649143 * 1e18, - borrowerCollateral: 0, - borrowert0Np: 10.307611531622595991 * 1e18, - borrowerCollateralization: 0 - } - ); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 7_064.430823099934649143 * 1e18, + borrowerCollateral: 0, + borrowert0Np: 10.307611531622595991 * 1e18, + borrowerCollateralization: 0 + }); + _assertKicker({ + kicker: _lender, + claimable: 0, + locked: 105.065056948053351817 * 1e18 // locked bond + reward, auction is not yet finalized + }); + // clear remaining debt - _settle( - { - from: _lender, - borrower: _borrower2, - maxDepth: 5, - settledDebt: 6_967.959658457733469639 * 1e18 - } - ); + _settle({ + from: _lender, + borrower: _borrower2, + maxDepth: 5, + settledDebt: 6_967.959658457733469639 * 1e18 + }); + _assertAuction( AuctionParams({ borrower: _borrower2, @@ -1919,61 +1645,55 @@ contract ERC20PoolLiquidationsTakeTest is ERC20HelperContract { neutralPrice: 0 }) ); - _assertKicker( - { - kicker: _lender, - claimable: 105.065056948053351817 * 1e18, - locked: 0 - } - ); - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 0, - borrowerCollateral: 0, - borrowert0Np: 10.307611531622595991 * 1e18, - borrowerCollateralization: 1 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 0, + borrowerCollateral: 0, + borrowert0Np: 10.307611531622595991 * 1e18, + borrowerCollateralization: 1 * 1e18 + }); + _assertKicker({ + kicker: _lender, + claimable: 105.065056948053351817 * 1e18, + locked: 0 + }); // kicker withdraws his auction bonds assertEq(_quote.balanceOf(_lender), 46_248.354604754094247543 * 1e18); - _pool.withdrawBonds(); + + _pool.withdrawBonds(_lender); + assertEq(_quote.balanceOf(_lender), 46_353.419661702147599360 * 1e18); - _assertKicker( - { - kicker: _lender, - claimable: 0, - locked: 0 - } - ); + + _assertKicker({ + kicker: _lender, + claimable: 0, + locked: 0 + }); } function testTakeReverts() external tearDown { // Borrower2 borrows - _borrow( - { - from: _borrower2, - amount: 1_730 * 1e18, - indexLimit: _i9_72, - newLup: 9.721295865031779605 * 1e18 - } - ); + _borrow({ + from: _borrower2, + amount: 1_730 * 1e18, + indexLimit: _i9_72, + newLup: 9.721295865031779605 * 1e18 + }); // Skip to make borrower undercollateralized skip(100 days); - _kick( - { - from: _lender, - borrower: _borrower2, - debt: 9_976.561670003961916237 * 1e18, - collateral: 1_000 * 1e18, - bond: 98.533942419792216457 * 1e18, - transferAmount: 98.533942419792216457 * 1e18 - } - ); + _kick({ + from: _lender, + borrower: _borrower2, + debt: 9_976.561670003961916237 * 1e18, + collateral: 1_000 * 1e18, + bond: 98.533942419792216457 * 1e18, + transferAmount: 98.533942419792216457 * 1e18 + }); + _assertAuction( AuctionParams({ borrower: _borrower2, @@ -1990,31 +1710,25 @@ contract ERC20PoolLiquidationsTakeTest is ERC20HelperContract { neutralPrice: 10.449783245217816340 * 1e18 }) ); - _assertKicker( - { - kicker: _lender, - claimable: 0, - locked: 98.533942419792216457 * 1e18 - } - ); - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 9_976.561670003961916237 * 1e18, - borrowerCollateral: 1_000 * 1e18, - borrowert0Np: 10.307611531622595991 * 1e18, - borrowerCollateralization: 0.974413448899967463 * 1e18 - } - ); - _assertReserveAuction( - { - reserves: 148.064352861909228810 * 1e18, - claimableReserves : 98.083873122003682866 * 1e18, - claimableReservesRemaining: 0, - auctionPrice: 0, - timeRemaining: 0 - } - ); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 9_976.561670003961916237 * 1e18, + borrowerCollateral: 1_000 * 1e18, + borrowert0Np: 10.307611531622595991 * 1e18, + borrowerCollateralization: 0.974413448899967463 * 1e18 + }); + _assertKicker({ + kicker: _lender, + claimable: 0, + locked: 98.533942419792216457 * 1e18 + }); + _assertReserveAuction({ + reserves: 148.064352861909228810 * 1e18, + claimableReserves : 98.083873122003682866 * 1e18, + claimableReservesRemaining: 0, + auctionPrice: 0, + timeRemaining: 0 + }); // Skip to make borrower undercollateralized skip(100 days); @@ -2035,27 +1749,22 @@ contract ERC20PoolLiquidationsTakeTest is ERC20HelperContract { neutralPrice: 0 }) ); - - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 19.776602251620519294 * 1e18, - borrowerCollateral: 2 * 1e18, - borrowert0Np: 10.115967548076923081 * 1e18, - borrowerCollateralization: 0.983110823724556080 * 1e18 - } - ); - - _kick( - { - from: _lender, - borrower: _borrower, - debt: 19.999089026951250136 * 1e18, - collateral: 2 * 1e18, - bond: 0.197766022516205193 * 1e18, - transferAmount: 0.197766022516205193 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 19.776602251620519294 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 10.115967548076923081 * 1e18, + borrowerCollateralization: 0.983110823724556080 * 1e18 + }); + + _kick({ + from: _lender, + borrower: _borrower, + debt: 19.999089026951250136 * 1e18, + collateral: 2 * 1e18, + bond: 0.197766022516205193 * 1e18, + transferAmount: 0.197766022516205193 * 1e18 + }); _assertAuction( AuctionParams({ @@ -2073,66 +1782,56 @@ contract ERC20PoolLiquidationsTakeTest is ERC20HelperContract { neutralPrice: 10.382716182100772629 * 1e18 }) ); - - _assertKicker( - { - kicker: _lender, - claimable: 0, - locked: 98.731708442308421650 * 1e18 - } - ); + _assertKicker({ + kicker: _lender, + claimable: 0, + locked: 98.731708442308421650 * 1e18 + }); skip(2 hours); // 10 borrowers draw debt to enable the min debt check for (uint i=0; i<10; ++i) { - _anonBorrowerDrawsDebt(1_000 * 1e18, 6_000 * 1e18, 7_777); - } + _anonBorrowerDrawsDebt(1_000 * 1e18, 6_000 * 1e18, MAX_FENWICK_INDEX); + } + // should revert if auction leaves borrower with debt under minimum pool debt - _assertTakeDebtUnderMinPoolDebtRevert( - { - from: _lender, - borrower: _borrower, - maxCollateral: 0.1 * 1e18 - } - ); + _assertTakeDebtUnderMinPoolDebtRevert({ + from: _lender, + borrower: _borrower, + maxCollateral: 0.1 * 1e18 + }); } function testTakeAuctionPriceLtNeutralPrice() external tearDown { - _addLiquidity( - { - from: _lender1, - amount: 1 * 1e18, - index: _i9_91, - lpAward: 1 * 1e27, - newLup: 9.721295865031779605 * 1e18 - } - ); + _addLiquidity({ + from: _lender1, + amount: 1 * 1e18, + index: _i9_91, + lpAward: 1 * 1e18, + newLup: 9.721295865031779605 * 1e18 + }); // Borrower2 borrows - _borrow( - { - from: _borrower2, - amount: 1_730 * 1e18, - indexLimit: _i9_72, - newLup: 9.721295865031779605 * 1e18 - } - ); + _borrow({ + from: _borrower2, + amount: 1_730 * 1e18, + indexLimit: _i9_72, + newLup: 9.721295865031779605 * 1e18 + }); // Skip to make borrower undercollateralized skip(100 days); - _kick( - { - from: _lender, - borrower: _borrower2, - debt: 9_976.561670003961916237 * 1e18, - collateral: 1_000 * 1e18, - bond: 98.533942419792216457 * 1e18, - transferAmount: 98.533942419792216457 * 1e18 - } - ); + _kick({ + from: _lender, + borrower: _borrower2, + debt: 9_976.561670003961916237 * 1e18, + collateral: 1_000 * 1e18, + bond: 98.533942419792216457 * 1e18, + transferAmount: 98.533942419792216457 * 1e18 + }); _assertAuction( AuctionParams({ @@ -2150,55 +1849,142 @@ contract ERC20PoolLiquidationsTakeTest is ERC20HelperContract { neutralPrice: 10.449783245217816340 * 1e18 }) ); - _assertKicker( - { - kicker: _lender, - claimable: 0, - locked: 98.533942419792216457 * 1e18 - } - ); - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 9_976.561670003961916237 * 1e18, - borrowerCollateral: 1_000 * 1e18, - borrowert0Np: 10.307611531622595991 * 1e18, - borrowerCollateralization: 0.974413448899967463 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 9_976.561670003961916237 * 1e18, + borrowerCollateral: 1_000 * 1e18, + borrowert0Np: 10.307611531622595991 * 1e18, + borrowerCollateralization: 0.974413448899967463 * 1e18 + }); + _assertKicker({ + kicker: _lender, + claimable: 0, + locked: 98.533942419792216457 * 1e18 + }); skip(3 hours); - _assertBucket( - { - index: _i9_91, - lpBalance: 2_001 * 1e27, - collateral: 0, - deposit: 2_119.781255869507381179 * 1e18, - exchangeRate: 1.059360947461023179000000000 * 1e27 - } - ); + _assertBucket({ + index: _i9_91, + lpBalance: 2_001 * 1e18, + collateral: 0, + deposit: 2_119.781255869507381179 * 1e18, + exchangeRate: 1.059360947461023179 * 1e18 + }); + + _take({ + from: _lender, + borrower: _borrower2, + maxCollateral: 1_001 * 1e18, + bondChange: 98.533942419792216457 * 1e18, + givenAmount: 10_675.085498940513902727 * 1e18, + collateralTaken: 127.695058936100465256 * 1e18, + isReward: false + }); + + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 0, + borrowerCollateral: 872.304941063899534744 * 1e18, + borrowert0Np: 0, + borrowerCollateralization: 1 * 1e18 + }); + } +} - _take( - { - from: _lender, - borrower: _borrower2, - maxCollateral: 1_001 * 1e18, - bondChange: 98.533942419792216457 * 1e18, - givenAmount: 10_675.085498940513902727 * 1e18, - collateralTaken: 127.695058936100465256 * 1e18, - isReward: false - } - ); +contract ERC20PoolLiquidationsTakeAndRepayAllDebtInPoolTest is ERC20HelperContract { + + address internal _lender; + address internal _borrower; + address internal _kicker; + address internal _taker; + + function setUp() external { + _lender = makeAddr("lender"); + _borrower = makeAddr("borrower"); + _kicker = makeAddr("kicker"); + _taker = makeAddr("taker"); + + _mintQuoteAndApproveTokens(_lender, 1_000_000 * 1e18); + _mintQuoteAndApproveTokens(_borrower, 1_000_000 * 1e18); + _mintQuoteAndApproveTokens(_kicker, 1_000_000 * 1e18); + _mintQuoteAndApproveTokens(_taker, 1_000_000 * 1e18); + + _mintCollateralAndApproveTokens(_borrower, 150_000 * 1e18); + + _addInitialLiquidity({ + from: _lender, + amount: 1_000 * 1e18, + index: 2690 + }); + _addInitialLiquidity({ + from: _lender, + amount: 1_000 * 1e18, + index: 2700 + }); + } + + function testTakeAuctionRepaidAmountGreaterThanPoolDebt() external tearDown { + _repayDebtNoLupCheck({ + from: _borrower, + borrower: _borrower, + amountToRepay: 0, + amountRepaid: 0, + collateralToPull: 0 + }); + + _drawDebtNoLupCheck({ + from: _borrower, + borrower: _borrower, + amountToBorrow: 635.189921955815900534 * 1e18, + limitIndex: 7000, + collateralToPledge: 0.428329945169804100 * 1e18 + }); + + skip(3276); + + _repayDebtNoLupCheck({ + from: _borrower, + borrower: _borrower, + amountToRepay: type(uint256).max, + amountRepaid: 635.803983894118939950 * 1e18, + collateralToPull: 0.428329945169804100 * 1e18 + }); + + _drawDebtNoLupCheck({ + from: _borrower, + borrower: _borrower, + amountToBorrow: 100 * 1e18, + limitIndex: 7000, + collateralToPledge: 0.067433366047580170 * 1e18 + }); + + skip(964); + skip(86400 * 200); + + _kick({ + from: _kicker, + borrower: _borrower, + debt: 104.162540773774892915 * 1e18, + collateral: 0.067433366047580170 * 1e18, + bond: 1.028765834802714992 * 1e18, + transferAmount: 1.028765834802714992 * 1e18 + }); + + skip(964); + skip(3600 * 3); + + // the calculated repaid amount is with 1 WAD greater than the pool debt + // check that take works and doesn't overflow + _take({ + from: _taker, + borrower: _borrower, + maxCollateral: 0.067433366047580170 * 1e18, + bondChange: 1.028765834802714992 * 1e18, + givenAmount: 111.455789568155429076 * 1e18, + collateralTaken: 0.010471063560951988 * 1e18, + isReward: false + }); - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 0, - borrowerCollateral: 872.304941063899534744 * 1e18, - borrowert0Np: 0, - borrowerCollateralization: 1 * 1e18 - } - ); } } diff --git a/tests/forge/ERC20Pool/ERC20PoolLoanHeap.t.sol b/tests/forge/ERC20Pool/ERC20PoolLoanHeap.t.sol new file mode 100644 index 000000000..c8fbbf988 --- /dev/null +++ b/tests/forge/ERC20Pool/ERC20PoolLoanHeap.t.sol @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.14; + +import { ERC20HelperContract } from './ERC20DSTestPlus.sol'; + +import 'src/libraries/helpers/PoolHelper.sol'; + +contract ERC20PoolLoanHeapTest is ERC20HelperContract { + + address internal _borrower1; + address internal _borrower2; + address internal _borrower3; + address internal _borrower4; + address internal _borrower5; + address internal _borrower6; + address internal _lender1; + address internal _lender2; + address internal _lender3; + address internal _lender4; + + function setUp() external { + _borrower1 = makeAddr("borrower1"); + _borrower2 = makeAddr("borrower2"); + _borrower3 = makeAddr("borrower3"); + _borrower4 = makeAddr("borrower4"); + _borrower5 = makeAddr("borrower5"); + _borrower6 = makeAddr("borrower6"); + _lender1 = makeAddr("lender1"); + _lender2 = makeAddr("lender2"); + _lender3 = makeAddr("lender3"); + _lender4 = makeAddr("lender4"); + + _mintQuoteAndApproveTokens(_lender1, 150_000 * 1e18); + _mintQuoteAndApproveTokens(_lender2, 150_000 * 1e18); + _mintQuoteAndApproveTokens(_lender3, 150_000 * 1e18); + _mintQuoteAndApproveTokens(_lender4, 5_000 * 1e18); + + _mintCollateralAndApproveTokens(_lender1, 1_000 * 1e18); + _mintCollateralAndApproveTokens(_lender3, 1_000 * 1e18); + _mintCollateralAndApproveTokens(_borrower1, 1_000 * 1e18); + _mintCollateralAndApproveTokens(_borrower2, 1_000 * 1e18); + _mintCollateralAndApproveTokens(_borrower3, 1_000 * 1e18); + _mintCollateralAndApproveTokens(_borrower4, 1_000 * 1e18); + _mintCollateralAndApproveTokens(_borrower5, 1_000 * 1e18); + _mintCollateralAndApproveTokens(_borrower6, 1_000 * 1e18); + + // Lender 1 adds Quote token accross 3 buckets + _addInitialLiquidity({ + from: _lender1, + amount: 50_000 * 1e18, + index: 2500 + }); + _addInitialLiquidity({ + from: _lender1, + amount: 50_000 * 1e18, + index: 2501 + }); + _addInitialLiquidity({ + from: _lender1, + amount: 1_000 * 1e18, + index: 2502 + }); + } + + function testLoanHeapUpdateThresholdPrice() external { + // all 6 borrowers draw debt from pool + _drawDebt({ + from: _borrower1, + borrower: _borrower1, + amountToBorrow: 1_000 * 1e18, + limitIndex: 5000, + collateralToPledge: 1_000 * 1e18, + newLup: 3_863.654368867279344664 * 1e18 + }); + _drawDebt({ + from: _borrower2, + borrower: _borrower2, + amountToBorrow: 2_000 * 1e18, + limitIndex: 5000, + collateralToPledge: 1_000 * 1e18, + newLup: 3_863.654368867279344664 * 1e18 + }); + _drawDebt({ + from: _borrower3, + borrower: _borrower3, + amountToBorrow: 3_000 * 1e18, + limitIndex: 5000, + collateralToPledge: 1_000 * 1e18, + newLup: 3_863.654368867279344664 * 1e18 + }); + _drawDebt({ + from: _borrower4, + borrower: _borrower4, + amountToBorrow: 4_000 * 1e18, + limitIndex: 5000, + collateralToPledge: 1_000 * 1e18, + newLup: 3_863.654368867279344664 * 1e18 + }); + _drawDebt({ + from: _borrower5, + borrower: _borrower5, + amountToBorrow: 5_000 * 1e18, + limitIndex: 5000, + collateralToPledge: 1_000 * 1e18, + newLup: 3_863.654368867279344664 * 1e18 + }); + _drawDebt({ + from: _borrower6, + borrower: _borrower6, + amountToBorrow: 6_000 * 1e18, + limitIndex: 5000, + collateralToPledge: 1_000 * 1e18, + newLup: 3_863.654368867279344664 * 1e18 + }); + + _assertLoans({ + noOfLoans: 6, + maxBorrower: _borrower6, + maxThresholdPrice: 6.005769230769230772 * 1e18 + }); + + // borrower 4 draws debt and becomes loan with highest threshold price in heap + _drawDebt({ + from: _borrower4, + borrower: _borrower4, + amountToBorrow: 10_000 * 1e18, + limitIndex: 5000, + collateralToPledge: 0, + newLup: 3_863.654368867279344664 * 1e18 + }); + + _assertLoans({ + noOfLoans: 6, + maxBorrower: _borrower4, + maxThresholdPrice: 14.013461538461538468 * 1e18 + }); + + // borrower 4 repays debt, borrower 6 becomes loan with highest threshold price in heap + _repayDebt({ + from: _borrower4, + borrower: _borrower4, + amountToRepay: 11_000 * 1e18, + amountRepaid: 11_000 * 1e18, + collateralToPull: 0, + newLup: 3_863.654368867279344664 * 1e18 + }); + + _assertLoans({ + noOfLoans: 6, + maxBorrower: _borrower6, + maxThresholdPrice: 6.005769230769230772 * 1e18 + }); + + // borrower 6 repays debt, borrower 5 becomes loan with highest threshold price in heap + _repayDebt({ + from: _borrower6, + borrower: _borrower6, + amountToRepay: 5_000 * 1e18, + amountRepaid: 5_000 * 1e18, + collateralToPull: 0, + newLup: 3_863.654368867279344664 * 1e18 + }); + + _assertLoans({ + noOfLoans: 6, + maxBorrower: _borrower5, + maxThresholdPrice: 5.004807692307692310 * 1e18 + }); + + // borrower 6 draws more debt and becomes loan with highest threshold price in heap + _drawDebt({ + from: _borrower6, + borrower: _borrower6, + amountToBorrow: 11_000 * 1e18, + limitIndex: 5000, + collateralToPledge: 0, + newLup: 3_863.654368867279344664 * 1e18 + }); + + _assertLoans({ + noOfLoans: 6, + maxBorrower: _borrower6, + maxThresholdPrice: 12.016346153846153854 * 1e18 + }); + } +} \ No newline at end of file diff --git a/tests/forge/ERC20Pool/ERC20PoolMulticall.t.sol b/tests/forge/ERC20Pool/ERC20PoolMulticall.t.sol index f4a61c935..26d758e5b 100644 --- a/tests/forge/ERC20Pool/ERC20PoolMulticall.t.sol +++ b/tests/forge/ERC20Pool/ERC20PoolMulticall.t.sol @@ -31,49 +31,49 @@ contract ERC20PoolMulticallTest is ERC20HelperContract { bytes[] memory callsToExecute = new bytes[](3); callsToExecute[0] = abi.encodeWithSignature( - "addQuoteToken(uint256,uint256)", + "addQuoteToken(uint256,uint256,uint256)", 10_000 * 1e18, - 2550 + 2550, + block.timestamp + 5 minutes ); callsToExecute[1] = abi.encodeWithSignature( - "addQuoteToken(uint256,uint256)", + "addQuoteToken(uint256,uint256,uint256)", 10_000 * 1e18, - 2551 + 2551, + block.timestamp + 5 minutes ); callsToExecute[2] = abi.encodeWithSignature( - "addQuoteToken(uint256,uint256)", + "addQuoteToken(uint256,uint256,uint256)", 10_000 * 1e18, - 2552 + 2552, + block.timestamp + 5 minutes ); changePrank(_lender); vm.expectEmit(true, true, false, true); - emit AddQuoteToken(_lender, 2550, 10_000 * 1e18, 10_000 * 1e27, MAX_PRICE); + emit AddQuoteToken(_lender, 2550, 10_000 * 1e18, 10_000 * 1e18, MAX_PRICE); vm.expectEmit(true, true, false, true); emit Transfer(_lender, address(_pool), 10_000 * 1e18); vm.expectEmit(true, true, false, true); - emit AddQuoteToken(_lender, 2551, 10_000 * 1e18, 10_000 * 1e27, MAX_PRICE); + emit AddQuoteToken(_lender, 2551, 10_000 * 1e18, 10_000 * 1e18, MAX_PRICE); vm.expectEmit(true, true, false, true); emit Transfer(_lender, address(_pool), 10_000 * 1e18); vm.expectEmit(true, true, false, true); - emit AddQuoteToken(_lender, 2552, 10_000 * 1e18, 10_000 * 1e27, MAX_PRICE); + emit AddQuoteToken(_lender, 2552, 10_000 * 1e18, 10_000 * 1e18, MAX_PRICE); vm.expectEmit(true, true, false, true); emit Transfer(_lender, address(_pool), 10_000 * 1e18); ERC20Pool(address(_pool)).multicall(callsToExecute); - - _assertPoolPrices( - { + _assertPoolPrices({ htp: 0, htpIndex: 7388, hpb: 3_010.892022197881557845 * 1e18, hpbIndex: 2550, lup: MAX_PRICE, lupIndex: 0 - } - ); + }); // check balances assertEq(_quote.balanceOf(address(_pool)), 30_000 * 1e18); @@ -82,59 +82,47 @@ contract ERC20PoolMulticallTest is ERC20HelperContract { assertEq(_pool.depositSize(), 30_000 * 1e18); // check buckets - _assertBucket( - { - index: 2550, - lpBalance: 10_000 * 1e27, - collateral: 0, - deposit: 10_000 * 1e18, - exchangeRate: 1 * 1e27 - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: 2550, - lpBalance: 10_000 * 1e27, - depositTime: _startTime - } - ); - - _assertBucket( - { - index: 2551, - lpBalance: 10_000 * 1e27, - collateral: 0, - deposit: 10_000 * 1e18, - exchangeRate: 1 * 1e27 - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: 2551, - lpBalance: 10_000 * 1e27, - depositTime: _startTime - } - ); - - _assertBucket( - { - index: 2552, - lpBalance: 10_000 * 1e27, - collateral: 0, - deposit: 10_000 * 1e18, - exchangeRate: 1 * 1e27 - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: 2552, - lpBalance: 10_000 * 1e27, - depositTime: _startTime - } - ); + _assertBucket({ + index: 2550, + lpBalance: 10_000 * 1e18, + collateral: 0, + deposit: 10_000 * 1e18, + exchangeRate: 1 * 1e18 + }); + _assertLenderLpBalance({ + lender: _lender, + index: 2550, + lpBalance: 10_000 * 1e18, + depositTime: _startTime + }); + + _assertBucket({ + index: 2551, + lpBalance: 10_000 * 1e18, + collateral: 0, + deposit: 10_000 * 1e18, + exchangeRate: 1 * 1e18 + }); + _assertLenderLpBalance({ + lender: _lender, + index: 2551, + lpBalance: 10_000 * 1e18, + depositTime: _startTime + }); + + _assertBucket({ + index: 2552, + lpBalance: 10_000 * 1e18, + collateral: 0, + deposit: 10_000 * 1e18, + exchangeRate: 1 * 1e18 + }); + _assertLenderLpBalance({ + lender: _lender, + index: 2552, + lpBalance: 10_000 * 1e18, + depositTime: _startTime + }); } function testMulticallRevertString() public { @@ -149,7 +137,7 @@ contract ERC20PoolMulticallTest is ERC20HelperContract { ); changePrank(_lender); - vm.expectRevert(IPoolErrors.LimitIndexReached.selector); + vm.expectRevert(IPoolErrors.LimitIndexExceeded.selector); ERC20Pool(address(_pool)).multicall(callsToExecute); } } diff --git a/tests/forge/ERC20Pool/ERC20PoolPrecision.t.sol b/tests/forge/ERC20Pool/ERC20PoolPrecision.t.sol index 8eb018c38..dbc6e5346 100644 --- a/tests/forge/ERC20Pool/ERC20PoolPrecision.t.sol +++ b/tests/forge/ERC20Pool/ERC20PoolPrecision.t.sol @@ -17,7 +17,7 @@ contract ERC20PoolPrecisionTest is ERC20DSTestPlus { uint256 internal constant MAX_DEPOSIT = 1e22 * 1e18; uint256 internal constant MAX_COLLATERAL = 1e12 * 1e18; uint256 internal constant POOL_PRECISION = 1e18; - uint256 internal constant LP_PRECISION = 1e27; + uint256 internal constant LP_PRECISION = 1e18; uint256 internal _collateralPrecision; uint256 internal _quotePrecision; @@ -91,125 +91,101 @@ contract ERC20PoolPrecisionTest is ERC20DSTestPlus { uint256 start = block.timestamp; // deposit 50_000 quote tokens into each of 3 buckets - _addInitialLiquidity( - { - from: _lender, - amount: 50_000 * POOL_PRECISION, - index: 2549 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 50_000 * POOL_PRECISION, - index: 2550 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 50_000 * POOL_PRECISION, - index: 2551 - } - ); + _addInitialLiquidity({ + from: _lender, + amount: 50_000 * POOL_PRECISION, + index: 2549 + }); + _addInitialLiquidity({ + from: _lender, + amount: 50_000 * POOL_PRECISION, + index: 2550 + }); + _addInitialLiquidity({ + from: _lender, + amount: 50_000 * POOL_PRECISION, + index: 2551 + }); // check balances assertEq(_quote.balanceOf(address(_pool)), 150_000 * _quotePrecision); assertEq(_quote.balanceOf(_lender), 50_000 * _quotePrecision); // check initial pool state - _assertPoolPrices( - { - htp: 0, - htpIndex: 7388, - hpb: 3_025.946482308870940904 * 1e18, - hpbIndex: 2549, - lup: MAX_PRICE, - lupIndex: 0 - } - ); - _assertLoans( - { - noOfLoans: 0, - maxBorrower: address(0), - maxThresholdPrice: 0 - } - ); + _assertPoolPrices({ + htp: 0, + htpIndex: 7388, + hpb: 3_025.946482308870940904 * 1e18, + hpbIndex: 2549, + lup: MAX_PRICE, + lupIndex: 0 + }); + _assertLoans({ + noOfLoans: 0, + maxBorrower: address(0), + maxThresholdPrice: 0 + }); assertEq(_pool.depositSize(), 150_000 * POOL_PRECISION); // check bucket balance - _assertBucket( - { - index: 2549, - lpBalance: 50_000 * 1e27, - collateral: 0, - deposit: 50_000 * POOL_PRECISION, - exchangeRate: 1 * LP_PRECISION - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: 2549, - lpBalance: 50_000 * 1e27, - depositTime: start - } - ); + _assertBucket({ + index: 2549, + lpBalance: 50_000 * 1e18, + collateral: 0, + deposit: 50_000 * POOL_PRECISION, + exchangeRate: 1 * LP_PRECISION + }); + _assertLenderLpBalance({ + lender: _lender, + index: 2549, + lpBalance: 50_000 * 1e18, + depositTime: start + }); skip(1 days); // skip to avoid penalty // lender removes some quote token from highest priced bucket - _removeLiquidity( - { - from: _lender, - amount: 25_000 * POOL_PRECISION, - index: 2549, - newLup: MAX_PRICE, - lpRedeem: 25_000 * 1e27 - } - ); + _removeLiquidity({ + from: _lender, + amount: 25_000 * POOL_PRECISION, + index: 2549, + newLup: MAX_PRICE, + lpRedeem: 25_000 * 1e18 + }); // check balances assertEq(_quote.balanceOf(address(_pool)), 125_000 * _quotePrecision); assertEq(_quote.balanceOf(_lender), 75_000 * _quotePrecision); // check pool state - _assertPoolPrices( - { - htp: 0, - htpIndex: 7388, - hpb: 3_025.946482308870940904 * 1e18, - hpbIndex: 2549, - lup: MAX_PRICE, - lupIndex: 0 - } - ); - _assertLoans( - { - noOfLoans: 0, - maxBorrower: address(0), - maxThresholdPrice: 0 - } - ); + _assertPoolPrices({ + htp: 0, + htpIndex: 7388, + hpb: 3_025.946482308870940904 * 1e18, + hpbIndex: 2549, + lup: MAX_PRICE, + lupIndex: 0 + }); + _assertLoans({ + noOfLoans: 0, + maxBorrower: address(0), + maxThresholdPrice: 0 + }); assertEq(_pool.depositSize(), 125_000 * POOL_PRECISION); // check bucket balance - _assertBucket( - { - index: 2549, - lpBalance: 25_000 * 1e27, - collateral: 0, - deposit: 25_000 * POOL_PRECISION, - exchangeRate: 1 * 1e27 - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: 2549, - lpBalance: 25_000 * LP_PRECISION, - depositTime: start - } - ); + _assertBucket({ + index: 2549, + lpBalance: 25_000 * 1e18, + collateral: 0, + deposit: 25_000 * POOL_PRECISION, + exchangeRate: 1 * 1e18 + }); + _assertLenderLpBalance({ + lender: _lender, + index: 2549, + lpBalance: 25_000 * LP_PRECISION, + depositTime: start + }); } function testAddRemoveCollateralPrecision ( @@ -266,36 +242,28 @@ contract ERC20PoolPrecisionTest is ERC20DSTestPlus { uint256 start = block.timestamp; - _addInitialLiquidity( - { - from: _lender, - amount: 50_000 * POOL_PRECISION, - index: 2549 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 50_000 * POOL_PRECISION, - index: 2550 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 50_000 * POOL_PRECISION, - index: 2551 - } - ); + _addInitialLiquidity({ + from: _lender, + amount: 50_000 * POOL_PRECISION, + index: 2549 + }); + _addInitialLiquidity({ + from: _lender, + amount: 50_000 * POOL_PRECISION, + index: 2550 + }); + _addInitialLiquidity({ + from: _lender, + amount: 50_000 * POOL_PRECISION, + index: 2551 + }); // borrowers adds collateral - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - amount: 50 * POOL_PRECISION - } - ); + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + amount: 50 * POOL_PRECISION + }); // check balances assertEq(_collateral.balanceOf(address(_pool)), 50 * _collateralPrecision); @@ -304,55 +272,45 @@ contract ERC20PoolPrecisionTest is ERC20DSTestPlus { assertEq(_quote.balanceOf(_borrower), 0); // check pool state - _assertPoolPrices( - { - htp: 0, - htpIndex: 7388, - hpb: 3_025.946482308870940904 * 1e18, - hpbIndex: 2549, - lup: MAX_PRICE, - lupIndex: 0 - } - ); - _assertLoans( - { - noOfLoans: 0, - maxBorrower: address(0), - maxThresholdPrice: 0 - } - ); + _assertPoolPrices({ + htp: 0, + htpIndex: 7388, + hpb: 3_025.946482308870940904 * 1e18, + hpbIndex: 2549, + lup: MAX_PRICE, + lupIndex: 0 + }); + _assertLoans({ + noOfLoans: 0, + maxBorrower: address(0), + maxThresholdPrice: 0 + }); assertEq(_pool.depositSize(), 150_000 * POOL_PRECISION); // check bucket balance - _assertBucket( - { - index: 2549, - lpBalance: 50_000 * LP_PRECISION, - collateral: 0, - deposit: 50_000 * POOL_PRECISION, - exchangeRate: 1 * LP_PRECISION - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: 2549, - lpBalance: 50_000 * LP_PRECISION, - depositTime: start - } - ); + _assertBucket({ + index: 2549, + lpBalance: 50_000 * LP_PRECISION, + collateral: 0, + deposit: 50_000 * POOL_PRECISION, + exchangeRate: 1 * LP_PRECISION + }); + _assertLenderLpBalance({ + lender: _lender, + index: 2549, + lpBalance: 50_000 * LP_PRECISION, + depositTime: start + }); // borrower borrows uint256 price = _priceAt(2549); - _borrow( - { - from: _borrower, - amount: 10_000 * POOL_PRECISION, - indexLimit: 3_000, - newLup: price - } - ); + _borrow({ + from: _borrower, + amount: 10_000 * POOL_PRECISION, + indexLimit: 3_000, + newLup: price + }); // check balances assertEq(_collateral.balanceOf(address(_pool)), 50 * _collateralPrecision); @@ -363,54 +321,47 @@ contract ERC20PoolPrecisionTest is ERC20DSTestPlus { // check pool state uint256 debt = 10_008.653846153846150000 * 1e18; uint256 col = 50 * 1e18; - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: debt, - borrowerCollateral: col, - borrowert0Np: 209.180865384615384535 * 1e18, - borrowerCollateralization: 15.116650694597107214 * 1e18 - } - ); - _assertPoolPrices( - { - htp: 200.173076923076923000 * 1e18, - htpIndex: 3093, - hpb: 3_025.946482308870940904 * 1e18, - hpbIndex: 2549, - lup: price, - lupIndex: 2549 - } - ); - _assertLoans( - { - noOfLoans: 1, - maxBorrower: _borrower, - maxThresholdPrice: 200.173076923076923000 * 1e18 - } - ); + + _assertBorrower({ + borrower: _borrower, + borrowerDebt: debt, + borrowerCollateral: col, + borrowert0Np: 209.180865384615384535 * 1e18, + borrowerCollateralization: 15.116650694597107214 * 1e18 + }); + _assertPoolPrices({ + htp: 200.173076923076923000 * 1e18, + htpIndex: 3093, + hpb: 3_025.946482308870940904 * 1e18, + hpbIndex: 2549, + lup: price, + lupIndex: 2549 + }); + _assertLoans({ + noOfLoans: 1, + maxBorrower: _borrower, + maxThresholdPrice: 200.173076923076923000 * 1e18 + }); + (uint256 poolDebt,,) = _pool.debtInfo(); + assertEq(_pool.depositSize(), 150_000 * POOL_PRECISION); assertEq(poolDebt, debt); assertEq(_pool.pledgedCollateral(), col); - _assertBucket( - { - index: 2549, - lpBalance: 50_000 * LP_PRECISION, - collateral: 0, - deposit: 50_000 * POOL_PRECISION, - exchangeRate: 1 * LP_PRECISION - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: 2549, - lpBalance: 50_000 * LP_PRECISION, - depositTime: start - } - ); + _assertBucket({ + index: 2549, + lpBalance: 50_000 * LP_PRECISION, + collateral: 0, + deposit: 50_000 * POOL_PRECISION, + exchangeRate: 1 * LP_PRECISION + }); + _assertLenderLpBalance({ + lender: _lender, + index: 2549, + lpBalance: 50_000 * LP_PRECISION, + depositTime: start + }); // borrower repays half of loan _repayDebt({ @@ -432,54 +383,47 @@ contract ERC20PoolPrecisionTest is ERC20DSTestPlus { // check pool state debt = 5_008.653846153846150000 * 1e18; col = 50 * 1e18; - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: debt, - borrowerCollateral: col, - borrowert0Np: 209.180865384615384535 * 1e18, - borrowerCollateralization: 30.207183159927296805 * 1e18 - } - ); - _assertPoolPrices( - { - htp: 100.173076923076923000 * 1e18, - htpIndex: 3232, - hpb: 3_025.946482308870940904 * 1e18, - hpbIndex: 2549, - lup: price, - lupIndex: 2549 - } - ); - _assertLoans( - { - noOfLoans: 1, - maxBorrower: _borrower, - maxThresholdPrice: 100.173076923076923000 * 1e18 - } - ); + + _assertBorrower({ + borrower: _borrower, + borrowerDebt: debt, + borrowerCollateral: col, + borrowert0Np: 209.180865384615384535 * 1e18, + borrowerCollateralization: 30.207183159927296805 * 1e18 + }); + _assertPoolPrices({ + htp: 100.173076923076923000 * 1e18, + htpIndex: 3232, + hpb: 3_025.946482308870940904 * 1e18, + hpbIndex: 2549, + lup: price, + lupIndex: 2549 + }); + _assertLoans({ + noOfLoans: 1, + maxBorrower: _borrower, + maxThresholdPrice: 100.173076923076923000 * 1e18 + }); + (poolDebt,,) = _pool.debtInfo(); + assertEq(_pool.depositSize(), 150_000 * 1e18); assertEq(poolDebt, debt); assertEq(_pool.pledgedCollateral(), col); - _assertBucket( - { - index: 2549, - lpBalance: 50_000 * LP_PRECISION, - collateral: 0, - deposit: 50_000 * 1e18, - exchangeRate: 1 * 1e27 - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: 2549, - lpBalance: 50_000 * LP_PRECISION, - depositTime: start - } - ); + _assertBucket({ + index: 2549, + lpBalance: 50_000 * LP_PRECISION, + collateral: 0, + deposit: 50_000 * 1e18, + exchangeRate: 1 * 1e18 + }); + _assertLenderLpBalance({ + lender: _lender, + index: 2549, + lpBalance: 50_000 * LP_PRECISION, + depositTime: start + }); // remove all of the remaining claimable collateral uint256 unencumberedCollateral = col - _encumberedCollateral(debt, _lup()); @@ -536,7 +480,7 @@ contract ERC20PoolPrecisionTest is ERC20DSTestPlus { lpBalance: 0, collateral: 0, deposit: 0, - exchangeRate: 1e27 + exchangeRate: 1e18 }); // addQuoteToken should add scaled quote token amount validate LP @@ -545,8 +489,9 @@ contract ERC20PoolPrecisionTest is ERC20DSTestPlus { amount: quoteAmount, index: bucketId }); + (uint256 lenderLpBalance, ) = _pool.lenderInfo(bucketId, _lender); - assertEq(lenderLpBalance, scaledQuoteAmount * 1e9); + assertEq(lenderLpBalance, scaledQuoteAmount); // deposit collateral and sanity check bidder LPs uint256 bidderLpBalance; @@ -614,11 +559,12 @@ contract ERC20PoolPrecisionTest is ERC20DSTestPlus { amount: scaledQuoteAmount1, index: bucketId }); + (uint256 lpBalance1, ) = _pool.lenderInfo(bucketId, _lender); // addQuoteToken should add scaled quote token amount and LP vm.expectEmit(true, true, false, true); - emit AddQuoteToken(lender2, bucketId, scaledQuoteAmount2, scaledQuoteAmount2 * 1e9, MAX_PRICE); + emit AddQuoteToken(lender2, bucketId, scaledQuoteAmount2, scaledQuoteAmount2, MAX_PRICE); _addLiquidityNoEventCheck(lender2, quoteAmount2, bucketId); (uint256 lpBalance2, ) = _pool.lenderInfo(bucketId, lender2); if (scaledQuoteAmount2 != 0) { @@ -654,58 +600,53 @@ contract ERC20PoolPrecisionTest is ERC20DSTestPlus { uint256 amountToMove = bound(uint256(amountToMove_), 0, _lenderDepositNormalized); init(boundColPrecision, boundQuotePrecision); - _addInitialLiquidity( - { - from: _lender, - amount: _lenderDepositNormalized, - index: fromBucketId - } - ); + _addInitialLiquidity({ + from: _lender, + amount: _lenderDepositNormalized, + index: fromBucketId + }); if (fromBucketId == toBucketId) { - _assertMoveLiquidityToSamePriceRevert( - { - from: _lender, - amount: amountToMove, - fromIndex: fromBucketId, - toIndex: toBucketId - } - ); + _assertMoveLiquidityToSamePriceRevert({ + from: _lender, + amount: amountToMove, + fromIndex: fromBucketId, + toIndex: toBucketId + }); + return; } if (amountToMove != 0 && amountToMove < _quoteDust) { - _assertMoveLiquidityDustRevert( - { - from: _lender, - amount: amountToMove, - fromIndex: fromBucketId, - toIndex: toBucketId - } - ); + _assertMoveLiquidityDustRevert({ + from: _lender, + amount: amountToMove, + fromIndex: fromBucketId, + toIndex: toBucketId + }); + return; } - _moveLiquidity( - { - from: _lender, - amount: amountToMove, - fromIndex: fromBucketId, - toIndex: toBucketId, - lpRedeemFrom: amountToMove * 1e9, - lpAwardTo: amountToMove * 1e9, - newLup: MAX_PRICE - } - ); + _moveLiquidity({ + from: _lender, + amount: amountToMove, + fromIndex: fromBucketId, + toIndex: toBucketId, + lpRedeemFrom: amountToMove, + lpAwardTo: amountToMove, + newLup: MAX_PRICE + }); // validate from and to buckets have appropriate amounts of deposit and LPs (, uint256 deposit,, uint256 lps,,) = _poolUtils.bucketInfo(address(_pool), fromBucketId); uint256 remaining = _lenderDepositNormalized - amountToMove; + assertEq(deposit, remaining); - assertEq(lps, remaining * 1e9); + assertEq(lps, remaining); (, deposit,, lps,,) = _poolUtils.bucketInfo(address(_pool), toBucketId); assertEq(deposit, amountToMove); - assertEq(lps, amountToMove * 1e9); + assertEq(lps, amountToMove); } function testDrawMinDebtAmount( @@ -767,8 +708,10 @@ contract ERC20PoolPrecisionTest is ERC20DSTestPlus { // have last borrower attempt an bad repay before tearDown (uint256 minDebtAmount, , , ) = _poolUtils.poolUtilizationInfo(address(_pool)); assertGt(minDebtAmount, 1); + (uint256 debt, , ) = _poolUtils.borrowerInfo(address(_pool), borrower); uint256 repayAmount = debt - minDebtAmount / 2; + _assertRepayMinDebtRevert({ from: borrower, borrower: borrower, @@ -843,8 +786,10 @@ contract ERC20PoolPrecisionTest is ERC20DSTestPlus { collateralToPledge: collateralToPledge, newLup: _priceAt(bucketId) }); + (uint256 currentDebt, uint256 pledgedCollateral, ) = _poolUtils.borrowerInfo(address(_pool), _borrower); assertGt(currentDebt, debtToDraw); + // round the collateral amount to token precision uint256 collateralRounded = (collateralToPledge / collateralScale) * collateralScale; assertEq(pledgedCollateral, collateralRounded); @@ -853,6 +798,32 @@ contract ERC20PoolPrecisionTest is ERC20DSTestPlus { skip(1 weeks); } + function testFlashLoanPrecision( + uint8 collateralPrecisionDecimals_, + uint8 quotePrecisionDecimals_ + ) external tearDown { + // setup fuzzy bounds and initialize the pool + uint256 collateralDecimals = bound(uint256(collateralPrecisionDecimals_), 1, 18); + uint256 quoteDecimals = bound(uint256(quotePrecisionDecimals_), 1, 18); + init(collateralDecimals, quoteDecimals); + + // add liquidity + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2500 + }); + + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + amount: 150 * 1e18 + }); + + assertEq(_pool.maxFlashLoan(address(_collateral)), 150 * 10 ** collateralDecimals); + assertEq(_pool.maxFlashLoan(address(_quote)), 10_000 * 10 ** quoteDecimals); + } + /**********************/ /*** Helper Methods ***/ @@ -913,4 +884,24 @@ contract ERC20PoolPrecisionTest is ERC20DSTestPlus { newLup: MAX_PRICE }); } + + function testMoveQuoteDustAmountRevert() external virtual tearDown { + init(8, 6); + + _addInitialLiquidity({ + from: _lender, + amount: 50_000 * 1e6, + index: 2550 + }); + + assertEq(_quoteDust, 0.000001 * 1e18); + + _assertMoveLiquidityDustRevert({ + from: _lender, + amount: 0.00000001 * 1e18, + fromIndex: 2550, + toIndex: 2551 + }); + } + } \ No newline at end of file diff --git a/tests/forge/ERC20Pool/ERC20PoolPurchaseQuote.t.sol b/tests/forge/ERC20Pool/ERC20PoolPurchaseQuote.t.sol index a3e83fbe5..6806ef005 100644 --- a/tests/forge/ERC20Pool/ERC20PoolPurchaseQuote.t.sol +++ b/tests/forge/ERC20Pool/ERC20PoolPurchaseQuote.t.sol @@ -33,93 +33,78 @@ contract ERC20PoolPurchaseQuoteTokenTest is ERC20HelperContract { uint256 testIndex = 2550; // lender adds initial quote to pool - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: testIndex - } - ); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: testIndex + }); // bidder deposits collateral into a bucket uint256 collateralToPurchaseWith = 4 * 1e18; - _addCollateral( - { - from: _bidder, - amount: collateralToPurchaseWith, - index: testIndex, - lpAward: 12_043.56808879152623138 * 1e27 - } - ); + + _addCollateral({ + from: _bidder, + amount: collateralToPurchaseWith, + index: testIndex, + lpAward: 12_043.56808879152623138 * 1e18 + }); // check bucket state and LPs - _assertBucket( - { - index: testIndex, - lpBalance: 22_043.56808879152623138 * 1e27, - collateral: collateralToPurchaseWith, - deposit: 10_000 * 1e18, - exchangeRate: 1 * 1e27 - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: testIndex, - lpBalance: 10_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: _bidder, - index: testIndex, - lpBalance: 12_043.56808879152623138 * 1e27, - depositTime: _startTime - } - ); + _assertBucket({ + index: testIndex, + lpBalance: 22_043.56808879152623138 * 1e18, + collateral: collateralToPurchaseWith, + deposit: 10_000 * 1e18, + exchangeRate: 1 * 1e18 + }); + _assertLenderLpBalance({ + lender: _lender, + index: testIndex, + lpBalance: 10_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: _bidder, + index: testIndex, + lpBalance: 12_043.56808879152623138 * 1e18, + depositTime: _startTime + }); uint256 availableCollateral = collateralToPurchaseWith; skip(1 days); // skip to avoid penalty + // bidder uses their LP to purchase all quote token in the bucket - _removeLiquidity( - { - from: _bidder, - amount: 10_000 * 1e18, - index: testIndex, - newLup: _lup(), - lpRedeem: 10_000 * 1e27 - } - ); + _removeLiquidity({ + from: _bidder, + amount: 10_000 * 1e18, + index: testIndex, + newLup: _lup(), + lpRedeem: 10_000 * 1e18 + }); + assertEq(_quote.balanceOf(_bidder), 10_000 * 1e18); // check bucket state - _assertBucket( - { - index: testIndex, - lpBalance: 12_043.56808879152623138 * 1e27, - collateral: collateralToPurchaseWith, - deposit: 0, - exchangeRate: 1 * 1e27 - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: testIndex, - lpBalance: 10_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: _bidder, - index: testIndex, - lpBalance: 2_043.56808879152623138 * 1e27, - depositTime: _startTime - } - ); + _assertBucket({ + index: testIndex, + lpBalance: 12_043.56808879152623138 * 1e18, + collateral: collateralToPurchaseWith, + deposit: 0, + exchangeRate: 1 * 1e18 + }); + _assertLenderLpBalance({ + lender: _lender, + index: testIndex, + lpBalance: 10_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: _bidder, + index: testIndex, + lpBalance: 2_043.56808879152623138 * 1e18, + depositTime: _startTime + }); // check pool state and balances assertEq(_collateral.balanceOf(_lender), 0); @@ -128,82 +113,67 @@ contract ERC20PoolPurchaseQuoteTokenTest is ERC20HelperContract { assertEq(_quote.balanceOf(address(_pool)), 0); // lender exchanges their LP for collateral - _removeAllCollateral( - { - from: _lender, - amount: 3.321274866808485288 * 1e18, - index: testIndex, - lpRedeem: 10_000 * 1e27 - } - ); - - _assertBucket( - { - index: testIndex, - lpBalance: 2_043.56808879152623138 * 1e27, - collateral: 0.678725133191514712 * 1e18, - deposit: 0, - exchangeRate: 0.999999999999999999892795209 * 1e27 - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: testIndex, - lpBalance: 0, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: _bidder, - index: testIndex, - lpBalance: 2_043.56808879152623138 * 1e27, - depositTime: _startTime - } - ); + _removeAllCollateral({ + from: _lender, + amount: 3.321274866808485288 * 1e18, + index: testIndex, + lpRedeem: 10_000 * 1e18 + }); + + _assertBucket({ + index: testIndex, + lpBalance: 2_043.56808879152623138 * 1e18, + collateral: 0.678725133191514712 * 1e18, + deposit: 0, + exchangeRate: 1 * 1e18 + }); + _assertLenderLpBalance({ + lender: _lender, + index: testIndex, + lpBalance: 0, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: _bidder, + index: testIndex, + lpBalance: 2_043.56808879152623138 * 1e18, + depositTime: _startTime + }); assertEq(_collateral.balanceOf(_lender), 3.321274866808485288 * 1e18); // bidder removes their _collateral - _removeAllCollateral( - { - from: _bidder, - amount: 0.678725133191514712 * 1e18, - index: testIndex, - lpRedeem: 2_043.56808879152623138 * 1e27 - } - ); + _removeAllCollateral({ + from: _bidder, + amount: 0.678725133191514712 * 1e18, + index: testIndex, + lpRedeem: 2_043.568088791526231161 * 1e18 + }); + // check pool balances assertEq(_collateral.balanceOf(address(_pool)), 0); assertEq(_quote.balanceOf(address(_pool)), 0); // check bucket state - _assertBucket( - { - index: testIndex, - lpBalance: 0, - collateral: 0, - deposit: 0, - exchangeRate: 1 * 1e27 - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: testIndex, - lpBalance: 0, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: _bidder, - index: testIndex, - lpBalance: 0, - depositTime: _startTime - } - ); + _assertBucket({ + index: testIndex, + lpBalance: 0, + collateral: 0, + deposit: 0, + exchangeRate: 1 * 1e18 + }); + _assertLenderLpBalance({ + lender: _lender, + index: testIndex, + lpBalance: 0, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: _bidder, + index: testIndex, + lpBalance: 0, + depositTime: _startTime + }); } /** @@ -214,62 +184,48 @@ contract ERC20PoolPurchaseQuoteTokenTest is ERC20HelperContract { // lenders add liquidity // lender 1 - _addInitialLiquidity( - { - from: _lender, - amount: 6_000 * 1e18, - index: 2550 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 2551 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 5_000 * 1e18, - index: 2552 - } - ); + _addInitialLiquidity({ + from: _lender, + amount: 6_000 * 1e18, + index: 2550 + }); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2551 + }); + _addInitialLiquidity({ + from: _lender, + amount: 5_000 * 1e18, + index: 2552 + }); // lender 2 - _addInitialLiquidity( - { - from: _lender1, - amount: 4_000 * 1e18, - index: 2550 - } - ); - _addInitialLiquidity( - { - from: _lender1, - amount: 5_000 * 1e18, - index: 2552 - } - ); + _addInitialLiquidity({ + from: _lender1, + amount: 4_000 * 1e18, + index: 2550 + }); + _addInitialLiquidity({ + from: _lender1, + amount: 5_000 * 1e18, + index: 2552 + }); skip(3600); // borrower draws debt - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - amount: 100 * 1e18 - } - ); - _borrow( - { - from: _borrower, - amount: 15_000 * 1e18, - indexLimit: 3_000, - newLup: _priceAt(2551) - } - ); + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + amount: 100 * 1e18 + }); + _borrow({ + from: _borrower, + amount: 15_000 * 1e18, + indexLimit: 3_000, + newLup: _priceAt(2551) + }); skip(86400); @@ -286,101 +242,88 @@ contract ERC20PoolPurchaseQuoteTokenTest is ERC20HelperContract { assertEq(collateralToPurchaseWith, 3.388032491631335842 * 1e18); // bidder purchases all quote from the highest bucket - _addCollateral( - { - from: _bidder, - amount: collateralToPurchaseWith, - index: 2550, - lpAward: 10_200.383861467480875668505869503 * 1e27 - } - ); + _addCollateral({ + from: _bidder, + amount: collateralToPurchaseWith, + index: 2550, + lpAward: 10_200.383861467480875669 * 1e18 + }); skip(25 hours); // remove liquidity after one day to avoid early withdraw penalty - _removeAllLiquidity( - { - from: _bidder, - amount: amountWithInterest, - index: 2550, - newLup: _priceAt(2552), - lpRedeem: 10_000.349513872212134187863727799 * 1e27 - } - ); + + _removeAllLiquidity({ + from: _bidder, + amount: amountWithInterest, + index: 2550, + newLup: _priceAt(2552), + lpRedeem: 10_000.349513872212134207 * 1e18 + }); // bidder withdraws unused collateral uint256 expectedCollateral = 0.066443194797165079 * 1e18; - _removeAllCollateral( - { - from: _bidder, - amount: expectedCollateral, - index: 2550, - lpRedeem: 200.034347595268741480642141704 * 1e27 - } - ); - _assertLenderLpBalance( - { - lender: _bidder, - index: 2550, - lpBalance: 0, - depositTime: _startTime + 3600 + 86400 - } - ); + + _removeAllCollateral({ + from: _bidder, + amount: expectedCollateral, + index: 2550, + lpRedeem: 200.034347595268741462 * 1e18 + }); + + _assertLenderLpBalance({ + lender: _bidder, + index: 2550, + lpBalance: 0, + depositTime: _startTime + 3600 + 86400 + }); skip(7200); // lender exchanges their LP for collateral expectedCollateral = 1.992953578100502458 * 1e18; - _removeAllCollateral( - { - from: _lender, - amount: expectedCollateral, - index: 2550, - lpRedeem: 6_000 * 1e27 - } - ); - - _assertLenderLpBalance( - { - lender: _lender, - index: 2550, - lpBalance: 0, - depositTime: _startTime - } - ); + + _removeAllCollateral({ + from: _lender, + amount: expectedCollateral, + index: 2550, + lpRedeem: 6_000 * 1e18 + }); + + _assertLenderLpBalance({ + lender: _lender, + index: 2550, + lpBalance: 0, + depositTime: _startTime + }); skip(3600); // lender1 exchanges their LP for collateral expectedCollateral = 1.328635718733668305 * 1e18; - _removeAllCollateral( - { - from: _lender1, - amount: expectedCollateral, - index: 2550, - lpRedeem: 4_000 * 1e27 - } - ); - - _assertLenderLpBalance( - { - lender: _lender1, - index: 2550, - lpBalance: 0, - depositTime: _startTime - } - ); + + _removeAllCollateral({ + from: _lender1, + amount: expectedCollateral, + index: 2550, + lpRedeem: 4_000 * 1e18 + }); + + _assertLenderLpBalance({ + lender: _lender1, + index: 2550, + lpBalance: 0, + depositTime: _startTime + }); // check pool balances assertEq(_collateral.balanceOf(address(_pool)), 100 * 1e18); // check bucket state - _assertBucket( - { - index: 2550, - lpBalance: 0, - collateral: 0, - deposit: 0, - exchangeRate: 1 * 1e27 - } - ); + _assertBucket({ + index: 2550, + lpBalance: 0, + collateral: 0, + deposit: 0, + exchangeRate: 1 * 1e18 + }); } } diff --git a/tests/forge/ERC20Pool/ERC20PoolQuoteToken.t.sol b/tests/forge/ERC20Pool/ERC20PoolQuoteToken.t.sol index 3ccda4596..bd072cf69 100644 --- a/tests/forge/ERC20Pool/ERC20PoolQuoteToken.t.sol +++ b/tests/forge/ERC20Pool/ERC20PoolQuoteToken.t.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.14; import { ERC20HelperContract } from './ERC20DSTestPlus.sol'; +import 'src/interfaces/pool/IPool.sol'; import 'src/libraries/helpers/PoolHelper.sol'; contract ERC20PoolQuoteTokenTest is ERC20HelperContract { @@ -33,22 +34,13 @@ contract ERC20PoolQuoteTokenTest is ERC20HelperContract { function testPoolDepositQuoteToken() external tearDown { assertEq(_hpb(), MIN_PRICE); - // should revert if trying to deposit at index 0 - _assertAddLiquidityAtIndex0Revert( - { - from: _lender, - amount: 10_000 * 1e18 - } - ); - // test 10_000 deposit at price of 3_010.892022197881557845 - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 2550 - } - ); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2550 + }); + _assertPool( PoolParams({ htp: 0, @@ -66,35 +58,31 @@ contract ERC20PoolQuoteTokenTest is ERC20HelperContract { interestRateUpdate: _startTime }) ); - _assertBucket( - { - index: 2550, - lpBalance: 10_000 * 1e27, - collateral: 0, - deposit: 10_000 * 1e18, - exchangeRate: 1 * 1e27 - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: 2550, - lpBalance: 10_000 * 1e27, - depositTime: _startTime - } - ); + _assertBucket({ + index: 2550, + lpBalance: 10_000 * 1e18, + collateral: 0, + deposit: 10_000 * 1e18, + exchangeRate: 1 * 1e18 + }); + _assertLenderLpBalance({ + lender: _lender, + index: 2550, + lpBalance: 10_000 * 1e18, + depositTime: _startTime + }); + // check balances assertEq(_quote.balanceOf(address(_pool)), 10_000 * 1e18); assertEq(_quote.balanceOf(_lender), 190_000 * 1e18); // test 20_000 deposit at price of 2_995.912459898389633881 - _addInitialLiquidity( - { - from: _lender, - amount: 20_000 * 1e18, - index: 2551 - } - ); + _addInitialLiquidity({ + from: _lender, + amount: 20_000 * 1e18, + index: 2551 + }); + _assertPool( PoolParams({ htp: 0, @@ -112,54 +100,44 @@ contract ERC20PoolQuoteTokenTest is ERC20HelperContract { interestRateUpdate: _startTime }) ); - - _assertBucket( - { - index: 2550, - lpBalance: 10_000 * 1e27, - collateral: 0, - deposit: 10_000 * 1e18, - exchangeRate: 1 * 1e27 - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: 2550, - lpBalance: 10_000 * 1e27, - depositTime: _startTime - } - ); - _assertBucket( - { - index: 2551, - lpBalance: 20_000 * 1e27, - collateral: 0, - deposit: 20_000 * 1e18, - exchangeRate: 1 * 1e27 - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: 2551, - lpBalance: 20_000 * 1e27, - depositTime: _startTime - } - ); + _assertBucket({ + index: 2550, + lpBalance: 10_000 * 1e18, + collateral: 0, + deposit: 10_000 * 1e18, + exchangeRate: 1 * 1e18 + }); + _assertLenderLpBalance({ + lender: _lender, + index: 2550, + lpBalance: 10_000 * 1e18, + depositTime: _startTime + }); + _assertBucket({ + index: 2551, + lpBalance: 20_000 * 1e18, + collateral: 0, + deposit: 20_000 * 1e18, + exchangeRate: 1 * 1e18 + }); + _assertLenderLpBalance({ + lender: _lender, + index: 2551, + lpBalance: 20_000 * 1e18, + depositTime: _startTime + }); // check balances assertEq(_quote.balanceOf(address(_pool)), 30_000 * 1e18); assertEq(_quote.balanceOf(_lender), 170_000 * 1e18); // test 40_000 deposit at price of 3_025.946482308870940904 DAI - _addInitialLiquidity( - { - from: _lender, - amount: 40_000 * 1e18, - index: 2549 - } - ); + _addInitialLiquidity({ + from: _lender, + amount: 40_000 * 1e18, + index: 2549 + }); + _assertPool( PoolParams({ htp: 0, @@ -177,92 +155,107 @@ contract ERC20PoolQuoteTokenTest is ERC20HelperContract { interestRateUpdate: _startTime }) ); - - _assertBucket( - { - index: 2549, - lpBalance: 40_000 * 1e27, - collateral: 0, - deposit: 40_000 * 1e18, - exchangeRate: 1 * 1e27 - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: 2549, - lpBalance: 40_000 * 1e27, - depositTime: _startTime - } - ); - _assertBucket( - { - index: 2550, - lpBalance: 10_000 * 1e27, - collateral: 0, - deposit: 10_000 * 1e18, - exchangeRate: 1 * 1e27 - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: 2550, - lpBalance: 10_000 * 1e27, - depositTime: _startTime - } - ); - _assertBucket( - { - index: 2551, - lpBalance: 20_000 * 1e27, - collateral: 0, - deposit: 20_000 * 1e18, - exchangeRate: 1 * 1e27 - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: 2551, - lpBalance: 20_000 * 1e27, - depositTime: _startTime - } - ); + _assertBucket({ + index: 2549, + lpBalance: 40_000 * 1e18, + collateral: 0, + deposit: 40_000 * 1e18, + exchangeRate: 1 * 1e18 + }); + _assertLenderLpBalance({ + lender: _lender, + index: 2549, + lpBalance: 40_000 * 1e18, + depositTime: _startTime + }); + _assertBucket({ + index: 2550, + lpBalance: 10_000 * 1e18, + collateral: 0, + deposit: 10_000 * 1e18, + exchangeRate: 1 * 1e18 + }); + _assertLenderLpBalance({ + lender: _lender, + index: 2550, + lpBalance: 10_000 * 1e18, + depositTime: _startTime + }); + _assertBucket({ + index: 2551, + lpBalance: 20_000 * 1e18, + collateral: 0, + deposit: 20_000 * 1e18, + exchangeRate: 1 * 1e18 + }); + _assertLenderLpBalance({ + lender: _lender, + index: 2551, + lpBalance: 20_000 * 1e18, + depositTime: _startTime + }); // check balances assertEq(_quote.balanceOf(address(_pool)), 70_000 * 1e18); assertEq(_quote.balanceOf(_lender), 130_000 * 1e18); } - function testPoolRemoveQuoteToken() external tearDown { - _addLiquidity( - { - from: _lender, - amount: 40_000 * 1e18, - index: 2549, - lpAward: 40_000 * 1e27, - newLup: MAX_PRICE - } - ); - _addLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 2550, - lpAward: 10_000 * 1e27, - newLup: MAX_PRICE - } - ); - _addLiquidity( - { - from: _lender, - amount: 20_000 * 1e18, - index: 2551, - lpAward: 20_000 * 1e27, - newLup: MAX_PRICE - } + function testPoolAddQuoteTokenReverts() external tearDown { + // should revert if trying to deposit at index 0 + _assertAddLiquidityAtIndex0Revert({ + from: _lender, + amount: 10_000 * 1e18 + }); + + // should revert if passing an already-expired timestamp + _assertAddLiquidityExpiredRevert({ + from: _lender, + amount: 100_000 * 1e18, + index: 3232, + expiry: block.timestamp - 1 minutes + }); + + // should revert if passing future timestamp but time has elapsed + bytes memory data = abi.encodeWithSignature( + "addQuoteToken(uint256,uint256,uint256)", + 50_000 * 1e18, + 3333, + block.timestamp + 5 minutes ); + + // should succeed if time hasn't passed + (bool success, ) = address(_pool).call(data); + assertEq(success, true); + + // should fail if expiration exceeded + skip(6 minutes); + vm.expectRevert(IPoolErrors.TransactionExpired.selector); + (success, ) = address(_pool).call(data); + } + + function testPoolRemoveQuoteToken() external tearDown { + _addLiquidity({ + from: _lender, + amount: 40_000 * 1e18, + index: 2549, + lpAward: 40_000 * 1e18, + newLup: MAX_PRICE + }); + _addLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2550, + lpAward: 10_000 * 1e18, + newLup: MAX_PRICE + }); + _addLiquidity( { + from: _lender, + amount: 20_000 * 1e18, + index: 2551, + lpAward: 20_000 * 1e18, + newLup: MAX_PRICE + }); + _assertPool( PoolParams({ htp: 0, @@ -280,73 +273,60 @@ contract ERC20PoolQuoteTokenTest is ERC20HelperContract { interestRateUpdate: _startTime }) ); - - _assertBucket( - { - index: 2549, - lpBalance: 40_000 * 1e27, - collateral: 0, - deposit: 40_000 * 1e18, - exchangeRate: 1 * 1e27 - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: 2549, - lpBalance: 40_000 * 1e27, - depositTime: _startTime - } - ); - _assertBucket( - { - index: 2550, - lpBalance: 10_000 * 1e27, - collateral: 0, - deposit: 10_000 * 1e18, - exchangeRate: 1 * 1e27 - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: 2550, - lpBalance: 10_000 * 1e27, - depositTime: _startTime - } - ); - _assertBucket( - { - index: 2551, - lpBalance: 20_000 * 1e27, - collateral: 0, - deposit: 20_000 * 1e18, - exchangeRate: 1 * 1e27 - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: 2551, - lpBalance: 20_000 * 1e27, - depositTime: _startTime - } - ); + _assertBucket({ + index: 2549, + lpBalance: 40_000 * 1e18, + collateral: 0, + deposit: 40_000 * 1e18, + exchangeRate: 1 * 1e18 + }); + _assertLenderLpBalance({ + lender: _lender, + index: 2549, + lpBalance: 40_000 * 1e18, + depositTime: _startTime + }); + _assertBucket({ + index: 2550, + lpBalance: 10_000 * 1e18, + collateral: 0, + deposit: 10_000 * 1e18, + exchangeRate: 1 * 1e18 + }); + _assertLenderLpBalance({ + lender: _lender, + index: 2550, + lpBalance: 10_000 * 1e18, + depositTime: _startTime + }); + _assertBucket({ + index: 2551, + lpBalance: 20_000 * 1e18, + collateral: 0, + deposit: 20_000 * 1e18, + exchangeRate: 1 * 1e18 + }); + _assertLenderLpBalance({ + lender: _lender, + index: 2551, + lpBalance: 20_000 * 1e18, + depositTime: _startTime + }); // check balances assertEq(_quote.balanceOf(address(_pool)), 70_000 * 1e18); assertEq(_quote.balanceOf(_lender), 130_000 * 1e18); skip(1 days); // skip to avoid penalty - _removeLiquidity( - { - from: _lender, - amount: 5_000 * 1e18, - index: 2549, - newLup: MAX_PRICE, - lpRedeem: 5_000 * 1e27 - } - ); + + _removeLiquidity({ + from: _lender, + amount: 5_000 * 1e18, + index: 2549, + newLup: MAX_PRICE, + lpRedeem: 5_000 * 1e18 + }); + _assertPool( PoolParams({ htp: 0, @@ -364,72 +344,58 @@ contract ERC20PoolQuoteTokenTest is ERC20HelperContract { interestRateUpdate: _startTime + 1 days }) ); - - _assertBucket( - { - index: 2549, - lpBalance: 35_000 * 1e27, - collateral: 0, - deposit: 35_000 * 1e18, - exchangeRate: 1 * 1e27 - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: 2549, - lpBalance: 35_000 * 1e27, - depositTime: _startTime - } - ); - _assertBucket( - { - index: 2550, - lpBalance: 10_000 * 1e27, - collateral: 0, - deposit: 10_000 * 1e18, - exchangeRate: 1 * 1e27 - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: 2550, - lpBalance: 10_000 * 1e27, - depositTime: _startTime - } - ); - _assertBucket( - { - index: 2551, - lpBalance: 20_000 * 1e27, - collateral: 0, - deposit: 20_000 * 1e18, - exchangeRate: 1 * 1e27 - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: 2551, - lpBalance: 20_000 * 1e27, - depositTime: _startTime - } - ); + _assertBucket({ + index: 2549, + lpBalance: 35_000 * 1e18, + collateral: 0, + deposit: 35_000 * 1e18, + exchangeRate: 1 * 1e18 + }); + _assertLenderLpBalance({ + lender: _lender, + index: 2549, + lpBalance: 35_000 * 1e18, + depositTime: _startTime + }); + _assertBucket({ + index: 2550, + lpBalance: 10_000 * 1e18, + collateral: 0, + deposit: 10_000 * 1e18, + exchangeRate: 1 * 1e18 + }); + _assertLenderLpBalance({ + lender: _lender, + index: 2550, + lpBalance: 10_000 * 1e18, + depositTime: _startTime + }); + _assertBucket({ + index: 2551, + lpBalance: 20_000 * 1e18, + collateral: 0, + deposit: 20_000 * 1e18, + exchangeRate: 1 * 1e18 + }); + _assertLenderLpBalance({ + lender: _lender, + index: 2551, + lpBalance: 20_000 * 1e18, + depositTime: _startTime + }); // check balances assertEq(_quote.balanceOf(address(_pool)), 65_000 * 1e18); assertEq(_quote.balanceOf(_lender), 135_000 * 1e18); - _removeLiquidity( - { - from: _lender, - amount: 35_000 * 1e18, - index: 2549, - newLup: MAX_PRICE, - lpRedeem: 35_000 * 1e27 - } - ); + _removeLiquidity({ + from: _lender, + amount: 35_000 * 1e18, + index: 2549, + newLup: MAX_PRICE, + lpRedeem: 35_000 * 1e18 + }); + _assertPool( PoolParams({ htp: 0, @@ -447,58 +413,45 @@ contract ERC20PoolQuoteTokenTest is ERC20HelperContract { interestRateUpdate: _startTime + 1 days }) ); - - _assertBucket( - { - index: 2549, - lpBalance: 0, - collateral: 0, - deposit: 0, - exchangeRate: 1 * 1e27 - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: 2549, - lpBalance: 0, - depositTime: _startTime - } - ); - _assertBucket( - { - index: 2550, - lpBalance: 10_000 * 1e27, - collateral: 0, - deposit: 10_000 * 1e18, - exchangeRate: 1 * 1e27 - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: 2550, - lpBalance: 10_000 * 1e27, - depositTime: _startTime - } - ); - _assertBucket( - { - index: 2551, - lpBalance: 20_000 * 1e27, - collateral: 0, - deposit: 20_000 * 1e18, - exchangeRate: 1 * 1e27 - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: 2551, - lpBalance: 20_000 * 1e27, - depositTime: _startTime - } - ); + _assertBucket({ + index: 2549, + lpBalance: 0, + collateral: 0, + deposit: 0, + exchangeRate: 1 * 1e18 + }); + _assertLenderLpBalance({ + lender: _lender, + index: 2549, + lpBalance: 0, + depositTime: _startTime + }); + _assertBucket({ + index: 2550, + lpBalance: 10_000 * 1e18, + collateral: 0, + deposit: 10_000 * 1e18, + exchangeRate: 1 * 1e18 + }); + _assertLenderLpBalance({ + lender: _lender, + index: 2550, + lpBalance: 10_000 * 1e18, + depositTime: _startTime + }); + _assertBucket({ + index: 2551, + lpBalance: 20_000 * 1e18, + collateral: 0, + deposit: 20_000 * 1e18, + exchangeRate: 1 * 1e18 + }); + _assertLenderLpBalance({ + lender: _lender, + index: 2551, + lpBalance: 20_000 * 1e18, + depositTime: _startTime + }); // check balances assertEq(_quote.balanceOf(address(_pool)), 30_000 * 1e18); @@ -512,32 +465,31 @@ contract ERC20PoolQuoteTokenTest is ERC20HelperContract { */ function testPoolRemoveQuoteTokenNotAvailable() external tearDown { _mintCollateralAndApproveTokens(_borrower, _collateral.balanceOf(_borrower) + 3_500_000 * 1e18); + // lender adds initial quote token - _addLiquidity( - { - from: _lender, - amount: 11_000 * 1e18, - index: 4550, - lpAward: 11_000 * 1e27, - newLup: MAX_PRICE - } - ); + _addLiquidity({ + from: _lender, + amount: 11_000 * 1e18, + index: 4550, + lpAward: 11_000 * 1e18, + newLup: MAX_PRICE + }); _drawDebt({ - from: _borrower, - borrower: _borrower, - amountToBorrow: 10_000 * 1e18, - limitIndex: 7000, + from: _borrower, + borrower: _borrower, + amountToBorrow: 10_000 * 1e18, + limitIndex: 7000, collateralToPledge: 3_500_000 * 1e18, - newLup: 0.140143083210662942 * 1e18 + newLup: 0.140143083210662942 * 1e18 }); _assertRemoveAllLiquidityLupBelowHtpRevert( - { - from: _lender, - index: 4550 - } - ); + { + from: _lender, + index: 4550 + } + ); } /** @@ -549,172 +501,147 @@ contract ERC20PoolQuoteTokenTest is ERC20HelperContract { function testPoolRemoveQuoteTokenReverts() external tearDown { _mintCollateralAndApproveTokens(_borrower, _collateral.balanceOf(_borrower) + 3_500_000 * 1e18); _mintCollateralAndApproveTokens(_lender, 1 * 1e18); + // lender adds initial quote token - _addLiquidity( - { - from: _lender, - amount: 41_000 * 1e18, - index: 4549, - lpAward: 41_000 * 1e27, - newLup: MAX_PRICE - } - ); - _addLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 4550, - lpAward: 10_000 * 1e27, - newLup: MAX_PRICE - } - ); - _addLiquidity( - { - from: _lender, - amount: 20_000 * 1e18, - index: 4551, - lpAward: 20_000 * 1e27, - newLup: MAX_PRICE - } - ); - _addLiquidity( - { - from: _lender, - amount: 30_000 * 1e18, - index: 4990, - lpAward: 30_000 * 1e27, - newLup: MAX_PRICE - } - ); + _addLiquidity({ + from: _lender, + amount: 41_000 * 1e18, + index: 4549, + lpAward: 41_000 * 1e18, + newLup: MAX_PRICE + }); + _addLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 4550, + lpAward: 10_000 * 1e18, + newLup: MAX_PRICE + }); + _addLiquidity({ + from: _lender, + amount: 20_000 * 1e18, + index: 4551, + lpAward: 20_000 * 1e18, + newLup: MAX_PRICE + }); + _addLiquidity( { + from: _lender, + amount: 30_000 * 1e18, + index: 4990, + lpAward: 30_000 * 1e18, + newLup: MAX_PRICE + }); + // add collateral in order to give lender LPs in bucket 5_000 with 0 deposit // used to test revert on remove when bucket deposit is 0 - _addCollateral( - { - from: _lender, - amount: 1 * 1e18, - index: 5000, - lpAward: 0.014854015662334135 * 1e27 - } - ); - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - amount: 3_500_000 * 1e18 - } - ); - _borrow( - { - from: _borrower, - amount: 70_000 * 1e18, - indexLimit: 7_000, - newLup: 0.139445853940958153 * 1e18 - } - ); + _addCollateral({ + from: _lender, + amount: 1 * 1e18, + index: 5000, + lpAward: 0.014854015662334135 * 1e18 + }); + + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + amount: 3_500_000 * 1e18 + }); + _borrow({ + from: _borrower, + amount: 70_000 * 1e18, + indexLimit: 7_000, + newLup: 0.139445853940958153 * 1e18 + }); // ensure lender cannot withdraw from a bucket with no deposit - _assertRemoveAllLiquidityNoClaimRevert( - { - from: _lender1, - index: 4550 - } - ); + _assertRemoveAllLiquidityNoClaimRevert({ + from: _lender1, + index: 4550 + }); + // should revert if no quote token in bucket deposit - _assertRemoveInsufficientLiquidityRevert( - { - from: _lender, - amount: 1 * 1e18, - index: 5000 - } - ); + _assertRemoveInsufficientLiquidityRevert({ + from: _lender, + amount: 1 * 1e18, + index: 5000 + }); + // should revert if removing quote token from higher price buckets would drive lup below htp - _assertRemoveLiquidityLupBelowHtpRevert( - { - from: _lender, - amount: 20_000 * 1e18, - index: 4551 - } - ); + _assertRemoveLiquidityLupBelowHtpRevert({ + from: _lender, + amount: 20_000 * 1e18, + index: 4551 + }); - _addLiquidity( - { - from: _lender1, - amount: 20_000 * 1e18, - index: 4550, - lpAward: 20_000 * 1e27, - newLup: _priceAt(4550) - } - ); + _addLiquidity({ + from: _lender1, + amount: 20_000 * 1e18, + index: 4550, + lpAward: 20_000 * 1e18, + newLup: _priceAt(4550) + }); skip(1 days); // skip to avoid penalty + // should be able to removeQuoteToken - _removeLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 4990, - newLup: _priceAt(4550), - lpRedeem: 10_000 * 1e27 - } - ); + _removeLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 4990, + newLup: _priceAt(4550), + lpRedeem: 10_000 * 1e18 + }); } function testPoolRemoveQuoteTokenWithCollateral() external { // add 10 collateral into the 100 bucket, for LP worth 1000 quote tokens _mintCollateralAndApproveTokens(_lender, 10 * 1e18); + uint256 i100 = _indexOf(100 * 1e18); - _addCollateral( - { - from: _lender, - amount: 10 * 1e18, - index: i100, - lpAward: 1003.3236814328200989 * 1e27 - } - ); - // someone else deposits into the bucket - _addLiquidity( - { - from: _lender1, - amount: 900 * 1e18, - index: i100, - lpAward: 900 * 1e27, - newLup: MAX_PRICE - } - ); + _addCollateral({ + from: _lender, + amount: 10 * 1e18, + index: i100, + lpAward: 1003.3236814328200989 * 1e18 + }); + + // another lender deposits into the bucket + _addLiquidity({ + from: _lender1, + amount: 900 * 1e18, + index: i100, + lpAward: 900 * 1e18, + newLup: MAX_PRICE + }); // should be able to remove a small amount of deposit skip(1 days); - _removeLiquidity( - { - from: _lender, - amount: 100 * 1e18, - index: i100, - newLup: MAX_PRICE, - lpRedeem: 100 * 1e27 - } - ); + + _removeLiquidity({ + from: _lender, + amount: 100 * 1e18, + index: i100, + newLup: MAX_PRICE, + lpRedeem: 100 * 1e18 + }); // should be able to remove the rest - _removeAllLiquidity( - { - from: _lender, - amount: 800 * 1e18, - index: i100, - newLup: MAX_PRICE, - lpRedeem: 800 * 1e27 - } - ); + _removeAllLiquidity({ + from: _lender, + amount: 800 * 1e18, + index: i100, + newLup: MAX_PRICE, + lpRedeem: 800 * 1e18 + }); - _assertBucket( - { - index: i100, - lpBalance: 1_003.3236814328200989 * 1e27, - collateral: 10 * 1e18, - deposit: 0, - exchangeRate: 1 * 1e27 - } - ); + _assertBucket({ + index: i100, + lpBalance: 1_003.3236814328200989 * 1e18, + collateral: 10 * 1e18, + deposit: 0, + exchangeRate: 1 * 1e18 + }); } function testPoolRemoveQuoteTokenWithDebt() external tearDown { @@ -723,59 +650,47 @@ contract ERC20PoolQuoteTokenTest is ERC20HelperContract { // lender adds initial quote token skip(1 minutes); // prevent deposit from having a zero timestamp - _addLiquidity( - { - from: _lender, - amount: 3_400 * 1e18, - index: 1606, - lpAward: 3_400 * 1e27, - newLup: MAX_PRICE - } - ); - _addLiquidity( - { - from: _lender, - amount: 3_400 * 1e18, - index: 1663, - lpAward: 3_400 * 1e27, - newLup: MAX_PRICE - } - ); + _addLiquidity({ + from: _lender, + amount: 3_400 * 1e18, + index: 1606, + lpAward: 3_400 * 1e18, + newLup: MAX_PRICE + }); + _addLiquidity({ + from: _lender, + amount: 3_400 * 1e18, + index: 1663, + lpAward: 3_400 * 1e18, + newLup: MAX_PRICE + }); - _assertBucket( - { - index: 1606, - lpBalance: 3_400 * 1e27, - collateral: 0, - deposit: 3_400 * 1e18, - exchangeRate: 1 * 1e27 - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: 1606, - lpBalance: 3_400 * 1e27, - depositTime: _startTime + 1 minutes - } - ); - _assertBucket( - { - index: 1663, - lpBalance: 3_400 * 1e27, - collateral: 0, - deposit: 3_400 * 1e18, - exchangeRate: 1 * 1e27 - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: 1663, - lpBalance: 3_400 * 1e27, - depositTime: _startTime + 1 minutes - } - ); + _assertBucket({ + index: 1606, + lpBalance: 3_400 * 1e18, + collateral: 0, + deposit: 3_400 * 1e18, + exchangeRate: 1 * 1e18 + }); + _assertLenderLpBalance({ + lender: _lender, + index: 1606, + lpBalance: 3_400 * 1e18, + depositTime: _startTime + 1 minutes + }); + _assertBucket({ + index: 1663, + lpBalance: 3_400 * 1e18, + collateral: 0, + deposit: 3_400 * 1e18, + exchangeRate: 1 * 1e18 + }); + _assertLenderLpBalance({ + lender: _lender, + index: 1663, + lpBalance: 3_400 * 1e18, + depositTime: _startTime + 1 minutes + }); skip(59 minutes); @@ -783,264 +698,218 @@ contract ERC20PoolQuoteTokenTest is ERC20HelperContract { // borrower takes a loan of 3000 quote token _drawDebt({ - from: _borrower, - borrower: _borrower, - amountToBorrow: 3_000 * 1e18, - limitIndex: 2_000, + from: _borrower, + borrower: _borrower, + amountToBorrow: 3_000 * 1e18, + limitIndex: 2_000, collateralToPledge: 100 * 1e18, - newLup: 333_777.824045947762079231 * 1e18 + newLup: 333_777.824045947762079231 * 1e18 }); skip(2 hours); - _assertLenderLpBalance( - { - lender: _lender, - index: 1663, - lpBalance: 3_400 * 1e27, - depositTime: _startTime + 1 minutes - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: 1663, - lpBalance: 3_400 * 1e27, - depositTime: _startTime + 1 minutes - } - ); + _assertLenderLpBalance({ + lender: _lender, + index: 1663, + lpBalance: 3_400 * 1e18, + depositTime: _startTime + 1 minutes + }); + _assertLenderLpBalance({ + lender: _lender, + index: 1663, + lpBalance: 3_400 * 1e18, + depositTime: _startTime + 1 minutes + }); // lender makes a partial withdrawal, paying an early withdrawal penalty - current annualized interest rate divided by 52 (one week of interest) (uint256 interestRate, ) = _pool.interestRateInfo(); uint256 penalty = Maths.WAD - Maths.wdiv(interestRate, 52 * 10**18); assertLt(penalty, Maths.WAD); + uint256 expectedWithdrawal1 = Maths.wmul(1_700 * 1e18, penalty); - _removeLiquidityWithPenalty( - { - from: _lender, - amount: 1_700 * 1e18, - amountRemoved: expectedWithdrawal1, - index: 1606, - newLup: _priceAt(1663), - lpRedeem: 1_699.988795593461528952000000000 * 1e27 - } - ); + + _removeLiquidityWithPenalty({ + from: _lender, + amount: 1_700 * 1e18, + amountRemoved: expectedWithdrawal1, + index: 1606, + newLup: _priceAt(1663), + lpRedeem: 1_699.988795593461528952000000000 * 1e18 + }); // lender removes all quote token, including interest, from the bucket skip(1 days); + assertGt(_priceAt(1606), _htp()); + uint256 expectedWithdrawal2 = 1_700.144243451229452671 * 1e18; - _removeAllLiquidity( - { - from: _lender, - amount: expectedWithdrawal2, - index: 1606, - newLup: _priceAt(1663), - lpRedeem: 1_700.011204406538471048000000000 * 1e27 - } - ); + + _removeAllLiquidity({ + from: _lender, + amount: expectedWithdrawal2, + index: 1606, + newLup: _priceAt(1663), + lpRedeem: 1_700.011204406538471048000000000 * 1e18 + }); + assertEq(_quote.balanceOf(_lender), lenderBalanceBefore + expectedWithdrawal1 + expectedWithdrawal2); - _assertBucket( - { - index: 1606, - lpBalance: 0, - collateral: 0, - deposit: 0, - exchangeRate: 1 * 1e27 - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: 1606, - lpBalance: 0, - depositTime: _startTime + 1 minutes - } - ); - _assertBucket( - { - index: 1663, - lpBalance: 3_400 * 1e27, - collateral: 0, - deposit: 3_400.266076335718765800 * 1e18, - exchangeRate: 1.000078257745799637000000000 * 1e27 - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: 1663, - lpBalance: 3_400 * 1e27, - depositTime: _startTime + 1 minutes - } - ); + _assertBucket({ + index: 1606, + lpBalance: 0, + collateral: 0, + deposit: 0, + exchangeRate: 1 * 1e18 + }); + _assertLenderLpBalance({ + lender: _lender, + index: 1606, + lpBalance: 0, + depositTime: _startTime + 1 minutes + }); + _assertBucket({ + index: 1663, + lpBalance: 3_400 * 1e18, + collateral: 0, + deposit: 3_400.266076335718765800 * 1e18, + exchangeRate: 1.000078257745799637000000000 * 1e18 + }); + _assertLenderLpBalance({ + lender: _lender, + index: 1663, + lpBalance: 3_400 * 1e18, + depositTime: _startTime + 1 minutes + }); } function testPoolMoveQuoteToken() external tearDown { - _addLiquidity( - { - from: _lender, - amount: 40_000 * 1e18, - index: 2549, - lpAward: 40_000 * 1e27, - newLup: MAX_PRICE - } - ); - _addLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 2550, - lpAward: 10_000 * 1e27, - newLup: MAX_PRICE - } - ); - _addLiquidity( - { - from: _lender, - amount: 20_000 * 1e18, - index: 2551, - lpAward: 20_000 * 1e27, - newLup: MAX_PRICE - } - ); + _addLiquidity({ + from: _lender, + amount: 40_000 * 1e18, + index: 2549, + lpAward: 40_000 * 1e18, + newLup: MAX_PRICE + }); + _addLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2550, + lpAward: 10_000 * 1e18, + newLup: MAX_PRICE + }); + _addLiquidity( { + from: _lender, + amount: 20_000 * 1e18, + index: 2551, + lpAward: 20_000 * 1e18, + newLup: MAX_PRICE + }); - _assertLenderLpBalance( - { - lender: _lender, - index: 2549, - lpBalance: 40_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: 2552, - lpBalance: 0, - depositTime: 0 - } - ); + _assertLenderLpBalance({ + lender: _lender, + index: 2549, + lpBalance: 40_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: _lender, + index: 2552, + lpBalance: 0, + depositTime: 0 + }); - _moveLiquidity( - { - from: _lender, - amount: 5_000 * 1e18, - fromIndex: 2549, - toIndex: 2552, - lpRedeemFrom: 5_000 * 1e27, - lpAwardTo: 5_000 * 1e27, - newLup: MAX_PRICE - } - ); + _moveLiquidity({ + from: _lender, + amount: 5_000 * 1e18, + fromIndex: 2549, + toIndex: 2552, + lpRedeemFrom: 5_000 * 1e18, + lpAwardTo: 5_000 * 1e18, + newLup: MAX_PRICE + }); - _assertLenderLpBalance( - { - lender: _lender, - index: 2549, - lpBalance: 35_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: 2552, - lpBalance: 5_000 * 1e27, - depositTime: _startTime - } - ); + _assertLenderLpBalance({ + lender: _lender, + index: 2549, + lpBalance: 35_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: _lender, + index: 2552, + lpBalance: 5_000 * 1e18, + depositTime: _startTime + }); - _moveLiquidity( - { - from: _lender, - amount: 5_000 * 1e18, - fromIndex: 2549, - toIndex: 2540, - lpRedeemFrom: 5_000 * 1e27, - lpAwardTo: 5_000 * 1e27, - newLup: MAX_PRICE - } - ); + _moveLiquidity({ + from: _lender, + amount: 5_000 * 1e18, + fromIndex: 2549, + toIndex: 2540, + lpRedeemFrom: 5_000 * 1e18, + lpAwardTo: 5_000 * 1e18, + newLup: MAX_PRICE + }); - _assertLenderLpBalance( - { - lender: _lender, - index: 2540, - lpBalance: 5_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: 2549, - lpBalance: 30_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: 2552, - lpBalance: 5_000 * 1e27, - depositTime: _startTime - } - ); + _assertLenderLpBalance({ + lender: _lender, + index: 2540, + lpBalance: 5_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: _lender, + index: 2549, + lpBalance: 30_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: _lender, + index: 2552, + lpBalance: 5_000 * 1e18, + depositTime: _startTime + }); - _moveLiquidity( - { - from: _lender, - amount: 15_000 * 1e18, - fromIndex: 2551, - toIndex: 2777, - lpRedeemFrom: 15_000 * 1e27, - lpAwardTo: 15_000 * 1e27, - newLup: MAX_PRICE - } - ); + _moveLiquidity({ + from: _lender, + amount: 15_000 * 1e18, + fromIndex: 2551, + toIndex: 2777, + lpRedeemFrom: 15_000 * 1e18, + lpAwardTo: 15_000 * 1e18, + newLup: MAX_PRICE + }); - _assertLenderLpBalance( - { - lender: _lender, - index: 2540, - lpBalance: 5_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: 2549, - lpBalance: 30_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: 2551, - lpBalance: 5_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: 2552, - lpBalance: 5_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: 2777, - lpBalance: 15_000 * 1e27, - depositTime: _startTime - } - ); + _assertLenderLpBalance({ + lender: _lender, + index: 2540, + lpBalance: 5_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: _lender, + index: 2549, + lpBalance: 30_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: _lender, + index: 2551, + lpBalance: 5_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: _lender, + index: 2552, + lpBalance: 5_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: _lender, + index: 2777, + lpBalance: 15_000 * 1e18, + depositTime: _startTime + }); } /** @@ -1056,134 +925,118 @@ contract ERC20PoolQuoteTokenTest is ERC20HelperContract { _mintCollateralAndApproveTokens(_borrower, _collateral.balanceOf(_lender1) + 1_500_000 * 1e18); // lender adds initial quote token - _addLiquidity( - { - from: _lender, - amount: 40_000 * 1e18, - index: 4549, - lpAward: 40_000 * 1e27, - newLup: MAX_PRICE - } - ); - _addLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 4550, - lpAward: 10_000 * 1e27, - newLup: MAX_PRICE - } - ); - _addLiquidity( - { - from: _lender, - amount: 20_000 * 1e18, - index: 4551, - lpAward: 20_000 * 1e27, - newLup: MAX_PRICE - } - ); - _addLiquidity( - { - from: _lender, - amount: 30_000 * 1e18, - index: 4651, - lpAward: 30_000 * 1e27, - newLup: MAX_PRICE - } - ); + _addLiquidity({ + from: _lender, + amount: 40_000 * 1e18, + index: 4549, + lpAward: 40_000 * 1e18, + newLup: MAX_PRICE + }); + _addLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 4550, + lpAward: 10_000 * 1e18, + newLup: MAX_PRICE + }); + _addLiquidity({ + from: _lender, + amount: 20_000 * 1e18, + index: 4551, + lpAward: 20_000 * 1e18, + newLup: MAX_PRICE + }); + _addLiquidity({ + from: _lender, + amount: 30_000 * 1e18, + index: 4651, + lpAward: 30_000 * 1e18, + newLup: MAX_PRICE + }); // should revert if moving quote token to the existing price - _assertMoveLiquidityToSamePriceRevert( - { - from: _lender, - amount: 5_000 * 1e18, - fromIndex: 4549, - toIndex: 4549 - } - ); + _assertMoveLiquidityToSamePriceRevert({ + from: _lender, + amount: 5_000 * 1e18, + fromIndex: 4549, + toIndex: 4549 + }); // should revert if moving quote token to index 0 - _assertMoveLiquidityToIndex0Revert( - { - from: _lender, - amount: 5_000 * 1e18, - fromIndex: 4549 - } - ); + _assertMoveLiquidityToIndex0Revert({ + from: _lender, + amount: 5_000 * 1e18, + fromIndex: 4549 + }); // borrow all available quote in the higher priced original 3 buckets, as well as some of the new lowest price bucket - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - amount: 1_500_000 * 1e18 - } - ); - _borrow( - { - from: _borrower, - amount: 60_000.1 * 1e18, - indexLimit: 4_651, - newLup: 0.139445853940958153 * 1e18 - } - ); + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + amount: 1_500_000 * 1e18 + }); + _borrow({ + from: _borrower, + amount: 60_000.1 * 1e18, + indexLimit: 4_651, + newLup: 0.139445853940958153 * 1e18 + }); // should revert if movement would drive lup below htp - _assertMoveLiquidityLupBelowHtpRevert( - { - from: _lender, - amount: 40_000 * 1e18, - fromIndex: 4549, - toIndex: 6000 - } - ); + _assertMoveLiquidityLupBelowHtpRevert({ + from: _lender, + amount: 40_000 * 1e18, + fromIndex: 4549, + toIndex: 6000 + }); + + // should revert if transaction expired + _assertMoveLiquidityExpiredRevert({ + from: _lender, + amount: 30_000 * 1e18, + fromIndex: 4549, + toIndex: 4459, + expiry: block.timestamp - 20 + }); // should be able to moveQuoteToken if properly specified - _moveLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - fromIndex: 4549, - toIndex: 4550, - lpRedeemFrom: 10_000 * 1e27, - lpAwardTo: 10_000 * 1e27, - newLup: _priceAt(4551) - } - ); + _moveLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + fromIndex: 4549, + toIndex: 4550, + lpRedeemFrom: 10_000 * 1e18, + lpAwardTo: 10_000 * 1e18, + newLup: _priceAt(4551) + }); } function testMoveQuoteTokenWithDebt() external tearDown { // lender makes an initial deposit skip(1 hours); - _addLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 2873, - lpAward: 10_000 * 1e27, - newLup: MAX_PRICE - } - ); + _addLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2873, + lpAward: 10_000 * 1e18, + newLup: MAX_PRICE + }); // borrower draws debt, establishing a pool threshold price skip(2 hours); - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - amount: 10 * 1e18 - } - ); - _borrow( - { - from: _borrower, - amount: 5_000 * 1e18, - indexLimit: 3_000, - newLup: 601.252968524772188572 * 1e18 - } - ); + + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + amount: 10 * 1e18 + }); + _borrow({ + from: _borrower, + amount: 5_000 * 1e18, + indexLimit: 3_000, + newLup: 601.252968524772188572 * 1e18 + }); (uint256 poolDebt,,) = _pool.debtInfo(); uint256 ptp = Maths.wdiv(poolDebt, 10 * 1e18); @@ -1191,79 +1044,165 @@ contract ERC20PoolQuoteTokenTest is ERC20HelperContract { // lender moves some liquidity below the pool threshold price; penalty should be assessed skip(16 hours); - _moveLiquidityWithPenalty( - { - from: _lender, - amount: 2_500 * 1e18, - amountMoved: 2_497.596153846153845 * 1e18, - fromIndex: 2873, - toIndex: 2954, - lpRedeemFrom: 2_499.899333909953254268000000000 * 1e27, - lpAwardTo: 2_497.596153846153845 * 1e27, - newLup: _lup() - } - ); + + _moveLiquidityWithPenalty({ + from: _lender, + amount: 2_500 * 1e18, + amountMoved: 2_497.596153846153845 * 1e18, + fromIndex: 2873, + toIndex: 2954, + lpRedeemFrom: 2_499.899333909953254268000000000 * 1e18, + lpAwardTo: 2_497.596153846153845 * 1e18, + newLup: _lup() + }); // another lender provides liquidity to prevent LUP from moving skip(1 hours); - _addLiquidity( - { - from: _lender1, - amount: 1_000 * 1e18, - index: 2873, - lpAward: 999.956320611641422442838174928 * 1e27, - newLup: 601.252968524772188572 * 1e18 - } - ); + _addLiquidity({ + from: _lender1, + amount: 1_000 * 1e18, + index: 2873, + lpAward: 999.956320611641422443 * 1e18, + newLup: 601.252968524772188572 * 1e18 + }); // lender moves more liquidity; no penalty assessed as sufficient time has passed skip(12 hours); - _moveLiquidity( - { - from: _lender, - amount: 2_500 * 1e18, - fromIndex: 2873, - toIndex: 2954, - lpRedeemFrom: 2_499.810182702901761330952408614 * 1e27, - lpAwardTo: 2_500 * 1e27, - newLup: _lup() - } - ); + + _moveLiquidity({ + from: _lender, + amount: 2_500 * 1e18, + fromIndex: 2873, + toIndex: 2954, + lpRedeemFrom: 2_499.810182702901761331 * 1e18, + lpAwardTo: 2_500 * 1e18, + newLup: _lup() + }); // after a week, another lender funds the pool skip(7 days); - _addLiquidity( - { - from: _lender1, - amount: 9_000 * 1e18, - index: 2873, - lpAward: 8_993.373316759001213153971860794 * 1e27, - newLup: 601.252968524772188572 * 1e18 - } - ); + _addLiquidity({ + from: _lender1, + amount: 9_000 * 1e18, + index: 2873, + lpAward: 8_993.373316759001213155 * 1e18, + newLup: 601.252968524772188572 * 1e18 + }); // lender removes all their quote, with interest skip(1 hours); - _removeAllLiquidity( - { - from: _lender, - amount: 5_003.981613396490344248 * 1e18, - index: 2873, - newLup: 601.252968524772188572 * 1e18, - lpRedeem: 5_000.290483387144984401047591386 * 1e27 - } - ); - _removeAllLiquidity( - { - from: _lender, - amount: 4_997.596153846153845 * 1e18, - index: 2954, - newLup: 601.252968524772188572 * 1e18, - lpRedeem: 4_997.596153846153845 * 1e27 - } - ); + + _removeAllLiquidity({ + from: _lender, + amount: 5_003.981613396490344248 * 1e18, + index: 2873, + newLup: 601.252968524772188572 * 1e18, + lpRedeem: 5_000.290483387144984401 * 1e18 + }); + + _removeAllLiquidity({ + from: _lender, + amount: 4_997.596153846153845 * 1e18, + index: 2954, + newLup: 601.252968524772188572 * 1e18, + lpRedeem: 4_997.596153846153845 * 1e18 + }); + assertGt(_quote.balanceOf(_lender), 200_000 * 1e18); } + + function testAddRemoveQuoteTokenBucketExchangeRateInvariantDifferentActor() tearDown external { + _mintQuoteAndApproveTokens(_lender, 1000000000000000000 * 1e18); + + uint256 initialLenderBalance = _quote.balanceOf(_lender); + + _addCollateral({ + from: _borrower, + amount: 13167, + index: 2570, + lpAward: 35880690 + }); + + _assertLenderLpBalance({ + lender: _borrower, + index: 2570, + lpBalance: 35880690, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: _lender, + index: 2570, + lpBalance: 0, + depositTime: 0 + }); + _assertBucket({ + index: 2570, + lpBalance: 35880690, + collateral: 13167, + deposit: 0, + exchangeRate: 1 * 1e18 + }); + + _addLiquidity({ + from: _lender, + amount: 984665640564039457.584007913129639933 * 1e18, + index: 2570, + lpAward: 984665640564039457.584007913129639933 * 1e18, + newLup: MAX_PRICE + }); + + _assertLenderLpBalance({ + lender: _borrower, + index: 2570, + lpBalance: 35880690, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: _lender, + index: 2570, + lpBalance: 984665640564039457.584007913129639933 * 1e18, + depositTime: _startTime + }); + _assertBucket({ + index: 2570, + lpBalance: 984665640564039457.584007913165520623 * 1e18, + collateral: 13167, + deposit: 984665640564039457.584007913129639933 * 1e18, + exchangeRate: 1 * 1e18 // exchange rate should not change + }); + + skip(48 hours); // to avoid penalty + + _removeAllLiquidity({ + from: _lender, + amount: 984665640564039457.584007913129639933 * 1e18, + index: 2570, + newLup: MAX_PRICE, + lpRedeem: 984665640564039457.584007913129639933 * 1e18 + }); + + _assertLenderLpBalance({ + lender: _borrower, + index: 2570, + lpBalance: 35880690, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: _lender, + index: 2570, + lpBalance: 0, // LPs should get back to same value as before add / remove collateral + depositTime: _startTime + }); + _assertBucket({ + index: 2570, + lpBalance: 35880690, + collateral: 13167, + deposit: 0, + exchangeRate: 1 * 1e18 // exchange rate should not change + }); + + assertEq(_quote.balanceOf(_lender), initialLenderBalance); + } } diff --git a/tests/forge/ERC20Pool/ERC20PoolReserveAuction.t.sol b/tests/forge/ERC20Pool/ERC20PoolReserveAuction.t.sol new file mode 100644 index 000000000..b0b7fcd63 --- /dev/null +++ b/tests/forge/ERC20Pool/ERC20PoolReserveAuction.t.sol @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.14; + +import '@openzeppelin/contracts/token/ERC20/ERC20.sol'; + +import { ERC20HelperContract } from './ERC20DSTestPlus.sol'; +import { FlashloanBorrower, SomeDefiStrategy } from '../utils/FlashloanBorrower.sol'; + +import 'src/libraries/helpers/PoolHelper.sol'; +import 'src/ERC20Pool.sol'; +import 'src/ERC20PoolFactory.sol'; + +import { IPoolErrors } from 'src/interfaces/pool/IPool.sol'; + +contract ERC20PoolReserveAuctionTest is ERC20HelperContract { + + ERC20 WBTC = ERC20(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599); + ERC20 USDC = ERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + ERC20 AJNA = ERC20(_ajna); + + address internal _borrower; + address internal _lender; + address internal _bidder; + + function setUp() external { + _pool = ERC20Pool(new ERC20PoolFactory(address(AJNA)).deployPool(address(WBTC), address(USDC), 0.05 * 10**18)); + + _borrower = makeAddr("borrower"); + _lender = makeAddr("lender"); + _bidder = makeAddr("bidder"); + + deal(address(WBTC), _borrower, 10 * 1e8); + deal(address(USDC), _borrower, 100 * 1e6); + + deal(address(USDC), _lender, 10_000 * 1e6); + + deal(address(AJNA), _bidder, 10 * 1e18); + + vm.startPrank(_borrower); + WBTC.approve(address(_pool), 10 * 1e18); + USDC.approve(address(_pool), 1_000 * 1e18); + + changePrank(_bidder); + AJNA.approve(address(_pool), 10 * 1e18); + + changePrank(_lender); + USDC.approve(address(_pool), 1_000 * 1e18); + + _addInitialLiquidity({ + from: _lender, + amount: 1_000 * 1e18, + index: 2500 + }); + + _drawDebtNoLupCheck({ + from: _borrower, + borrower: _borrower, + amountToBorrow: 300 * 1e18, + limitIndex: 7000, + collateralToPledge: 1 * 1e18 + }); + } + + function testStartAndTakeUsdcReserveAuction() external { + // skip time to accumulate interest + skip(26 weeks); + + // repay entire debt + _repayDebtNoLupCheck({ + from: _borrower, + borrower: _borrower, + amountToRepay: 400 * 1e18, + amountRepaid: 307.869212479869665749 * 1e18, + collateralToPull: 0 + }); + + assertEq(USDC.balanceOf(address(_borrower)), 92.130788 * 1e6); + assertEq(USDC.balanceOf(address(_pool)), 1_007.869212 * 1e6); + + _assertReserveAuction({ + reserves: 1.297969216344413 * 1e18, + claimableReserves : 1.297969216344413 * 1e18, + claimableReservesRemaining: 0, + auctionPrice: 0, + timeRemaining: 0 + }); + + // kick off a new auction + _startClaimableReserveAuction({ + from: _bidder, + remainingReserves: 1.284989524180968870 * 1e18, + price: 1000000000 * 1e18 + }); + + skip(60 hours); + + _assertReserveAuction({ + reserves: 0.000000692163444130 * 1e18, + claimableReserves : 0.000000692163444130 * 1e18, + claimableReservesRemaining: 1.284989524180968870 * 1e18, + auctionPrice: 0.000000000867361737 * 1e18, + timeRemaining: 43200 + }); + + assertEq(USDC.balanceOf(address(_pool)), 1_007.856233 * 1e6); + assertEq(USDC.balanceOf(address(_bidder)), 0.012979 * 1e6); // kicker reward + assertEq(AJNA.balanceOf(address(_bidder)), 10 * 1e18); + + _pool.takeReserves(10 * 1e18); + + assertEq(USDC.balanceOf(address(_pool)), 1_006.571244 * 1e6); + assertEq(USDC.balanceOf(address(_bidder)), 1.297968 * 1e6); + assertEq(AJNA.balanceOf(address(_bidder)), 9.999999998885449254 * 1e18); + } +} \ No newline at end of file diff --git a/tests/forge/ERC20Pool/ERC20PoolTransferLPTokens.t.sol b/tests/forge/ERC20Pool/ERC20PoolTransferLPTokens.t.sol deleted file mode 100644 index 89dca6f78..000000000 --- a/tests/forge/ERC20Pool/ERC20PoolTransferLPTokens.t.sol +++ /dev/null @@ -1,632 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.14; - -import { ERC20HelperContract } from './ERC20DSTestPlus.sol'; - -import 'src/libraries/helpers/PoolHelper.sol'; - -contract ERC20PoolTransferLPTokensTest is ERC20HelperContract { - - address internal _lender; - address internal _lender1; - address internal _lender2; - - function setUp() external { - _lender = makeAddr("lender"); - _lender1 = makeAddr("lender1"); - _lender2 = makeAddr("lender2"); - - _mintQuoteAndApproveTokens(_lender, 200_000 * 1e18); - _mintQuoteAndApproveTokens(_lender1, 200_000 * 1e18); - _mintQuoteAndApproveTokens(_lender2, 200_000 * 1e18); - } - - /********************************/ - /*** Transfer LP Tokens Tests ***/ - /********************************/ - - function testTransferLPTokensToZeroAddress() external tearDown { - uint256[] memory indexes = new uint256[](3); - indexes[0] = 2550; - indexes[1] = 2551; - indexes[2] = 2552; - - // should fail if allowed owner is not set - _assertTransferNoAllowanceRevert( - { - operator: _lender, - from: _lender1, - to: _lender2, - indexes: indexes - } - ); - - // should fail if allowed owner is set to 0x - changePrank(_lender1); - _pool.approveLpOwnership(address(0), indexes[0], 1_000 * 1e18); - - _assertTransferNoAllowanceRevert( - { - operator: _lender, - from: _lender1, - to: _lender2, - indexes: indexes - } - ); - } - - function testTransferLPTokensToUnallowedAddress() external tearDown { - uint256[] memory indexes = new uint256[](3); - indexes[0] = 2550; - indexes[1] = 2551; - indexes[2] = 2552; - - // should fail if allowed owner is set to lender2 address but trying to transfer to lender address - changePrank(_lender1); - _pool.approveLpOwnership(_lender2, indexes[0], 1_000 * 1e27); - _pool.approveLpOwnership(_lender2, indexes[1], 1_000 * 1e27); - _pool.approveLpOwnership(_lender2, indexes[2], 1_000 * 1e27); - - _assertTransferNoAllowanceRevert( - { - operator: _lender, - from: _lender1, - to: _lender2, - indexes: indexes - } - ); - } - - function testTransferLPTokensToInvalidIndex() external tearDown { - uint256[] memory indexes = new uint256[](3); - indexes[0] = 9999; - indexes[1] = 2550; - indexes[2] = 2552; - - // should fail since 9999 is not a valid index - changePrank(_lender1); - _pool.approveLpOwnership(_lender2, indexes[0], 1_000 * 1e27); - _pool.approveLpOwnership(_lender2, indexes[1], 1_000 * 1e27); - _pool.approveLpOwnership(_lender2, indexes[2], 1_000 * 1e27); - - _assertTransferInvalidIndexRevert( - { - operator: _lender, - from: _lender1, - to: _lender2, - indexes: indexes - } - ); - } - - function testTransferLPTokensGreaterThanBalance() external tearDown { - uint256[] memory indexes = new uint256[](2); - indexes[0] = 2550; - indexes[1] = 2551; - - _addInitialLiquidity( - { - from: _lender1, - amount: 10_000 * 1e18, - index: indexes[0] - } - ); - _addInitialLiquidity( - { - from: _lender1, - amount: 20_000 * 1e18, - index: indexes[1] - } - ); - - // set allowed owner to lender2 address - _pool.approveLpOwnership(_lender2, indexes[0], 10_000 * 1e27); - _pool.approveLpOwnership(_lender2, indexes[1], 30_000 * 1e27); - - _assertTransferNoAllowanceRevert( - { - operator: _lender2, - from: _lender1, - to: _lender2, - indexes: indexes - } - ); - } - - function testTransferLPTokensForAllIndexes() external tearDown { - uint256[] memory indexes = new uint256[](3); - indexes[0] = 2550; - indexes[1] = 2551; - indexes[2] = 2552; - - skip(1 hours); - - _addInitialLiquidity( - { - from: _lender1, - amount: 10_000 * 1e18, - index: indexes[0] - } - ); - _addInitialLiquidity( - { - from: _lender1, - amount: 20_000 * 1e18, - index: indexes[1] - } - ); - _addInitialLiquidity( - { - from: _lender1, - amount: 30_000 * 1e18, - index: indexes[2] - } - ); - - // check lenders lp balance - _assertLenderLpBalance( - { - lender: _lender1, - index: indexes[0], - lpBalance: 10_000 * 1e27, - depositTime: _startTime + 1 hours - } - ); - _assertLenderLpBalance( - { - lender: _lender2, - index: indexes[0], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: _lender1, - index: indexes[1], - lpBalance: 20_000 * 1e27, - depositTime: _startTime + 1 hours - } - ); - _assertLenderLpBalance( - { - lender: _lender2, - index: indexes[1], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: _lender1, - index: indexes[2], - lpBalance: 30_000 * 1e27, - depositTime: _startTime + 1 hours - } - ); - _assertLenderLpBalance( - { - lender: _lender2, - index: indexes[2], - lpBalance: 0, - depositTime: 0 - } - ); - - // set allowed owner to lender2 address - _pool.approveLpOwnership(_lender2, indexes[0], 10_000 * 1e27); - _pool.approveLpOwnership(_lender2, indexes[1], 20_000 * 1e27); - _pool.approveLpOwnership(_lender2, indexes[2], 30_000 * 1e27); - - // transfer LP tokens for all indexes - _transferLpTokens( - { - operator: _lender, - from: _lender1, - to: _lender2, - indexes: indexes, - lpBalance: 60_000 * 1e27 - } - ); - - // check that old token ownership was removed - a new transfer should fail - _assertTransferNoAllowanceRevert( - { - operator: _lender, - from: _lender1, - to: _lender2, - indexes: indexes - } - ); - - // check lenders lp balance - _assertLenderLpBalance( - { - lender: _lender1, - index: indexes[0], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: _lender2, - index: indexes[0], - lpBalance: 10_000 * 1e27, - depositTime: _startTime + 1 hours - } - ); - _assertLenderLpBalance( - { - lender: _lender1, - index: indexes[1], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: _lender2, - index: indexes[1], - lpBalance: 20_000 * 1e27, - depositTime: _startTime + 1 hours - } - ); - _assertLenderLpBalance( - { - lender: _lender1, - index: indexes[2], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: _lender2, - index: indexes[2], - lpBalance: 30_000 * 1e27, - depositTime: _startTime + 1 hours - } - ); - } - - function testTransferLPTokensForTwoIndexes() external tearDown { - uint256[] memory depositIndexes = new uint256[](3); - depositIndexes[0] = 2550; - depositIndexes[1] = 2551; - depositIndexes[2] = 2552; - - uint256[] memory transferIndexes = new uint256[](2); - transferIndexes[0] = 2550; - transferIndexes[1] = 2552; - - _addInitialLiquidity( - { - from: _lender1, - amount: 10_000 * 1e18, - index: depositIndexes[0] - } - ); - _addInitialLiquidity( - { - from: _lender1, - amount: 20_000 * 1e18, - index: depositIndexes[1] - } - ); - _addInitialLiquidity( - { - from: _lender1, - amount: 30_000 * 1e18, - index: depositIndexes[2] - } - ); - - // check lenders lp balance - _assertLenderLpBalance( - { - lender: _lender1, - index: depositIndexes[0], - lpBalance: 10_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: _lender2, - index: depositIndexes[0], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: _lender1, - index: depositIndexes[1], - lpBalance: 20_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: _lender2, - index: depositIndexes[1], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: _lender1, - index: depositIndexes[2], - lpBalance: 30_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: _lender2, - index: depositIndexes[2], - lpBalance: 0, - depositTime: 0 - } - ); - - // set allowed owner to lender2 address - _pool.approveLpOwnership(_lender2, transferIndexes[0], 10_000 * 1e27); - _pool.approveLpOwnership(_lender2, transferIndexes[1], 30_000 * 1e27); - - // transfer LP tokens for 2 indexes - _transferLpTokens( - { - operator: _lender, - from: _lender1, - to: _lender2, - indexes: transferIndexes, - lpBalance: 40_000 * 1e27 - } - ); - - // check that old token ownership was removed - transfer with same indexes should fail - _assertTransferNoAllowanceRevert( - { - operator: _lender, - from: _lender1, - to: _lender2, - indexes: transferIndexes - } - ); - - // check lenders lp balance - _assertLenderLpBalance( - { - lender: _lender1, - index: depositIndexes[0], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: _lender2, - index: depositIndexes[0], - lpBalance: 10_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: _lender1, - index: depositIndexes[1], - lpBalance: 20_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: _lender2, - index: depositIndexes[1], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: _lender1, - index: depositIndexes[2], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: _lender2, - index: depositIndexes[2], - lpBalance: 30_000 * 1e27, - depositTime: _startTime - } - ); - } - - function testTransferLPTokensToLenderWithLPTokens() external tearDown { - uint256[] memory indexes = new uint256[](3); - indexes[0] = 2550; - indexes[1] = 2551; - indexes[2] = 2552; - - skip(1 hours); - - _addInitialLiquidity( - { - from: _lender1, - amount: 10_000 * 1e18, - index: indexes[0] - } - ); - _addInitialLiquidity( - { - from: _lender1, - amount: 20_000 * 1e18, - index: indexes[1] - } - ); - _addInitialLiquidity( - { - from: _lender1, - amount: 30_000 * 1e18, - index: indexes[2] - } - ); - - skip(1 hours); - - _addInitialLiquidity( - { - from: _lender2, - amount: 5_000 * 1e18, - index: indexes[0] - } - ); - _addInitialLiquidity( - { - from: _lender2, - amount: 10_000 * 1e18, - index: indexes[1] - } - ); - _addInitialLiquidity( - { - from: _lender2, - amount: 15_000 * 1e18, - index: indexes[2] - } - ); - - // check lenders lp balance - _assertLenderLpBalance( - { - lender: _lender1, - index: indexes[0], - lpBalance: 10_000 * 1e27, - depositTime: _startTime + 1 hours - } - ); - _assertLenderLpBalance( - { - lender: _lender2, - index: indexes[0], - lpBalance: 5_000 * 1e27, - depositTime: _startTime + 2 hours - } - ); - _assertLenderLpBalance( - { - lender: _lender1, - index: indexes[1], - lpBalance: 20_000 * 1e27, - depositTime: _startTime + 1 hours - } - ); - _assertLenderLpBalance( - { - lender: _lender2, - index: indexes[1], - lpBalance: 10_000 * 1e27, - depositTime: _startTime + 2 hours - } - ); - _assertLenderLpBalance( - { - lender: _lender1, - index: indexes[2], - lpBalance: 30_000 * 1e27, - depositTime: _startTime + 1 hours - } - ); - _assertLenderLpBalance( - { - lender: _lender2, - index: indexes[2], - lpBalance: 15_000 * 1e27, - depositTime: _startTime + 2 hours - } - ); - - // set allowed owner to lender2 address - changePrank(_lender1); - _pool.approveLpOwnership(_lender2, indexes[0], 10_000 * 1e27); - _pool.approveLpOwnership(_lender2, indexes[1], 20_000 * 1e27); - _pool.approveLpOwnership(_lender2, indexes[2], 30_000 * 1e27); - - // transfer LP tokens for all indexes - _transferLpTokens( - { - operator: _lender, - from: _lender1, - to: _lender2, - indexes: indexes, - lpBalance: 60_000 * 1e27 - } - ); - - // check that old token ownership was removed - transfer with same indexes should fail - _assertTransferNoAllowanceRevert( - { - operator: _lender, - from: _lender1, - to: _lender2, - indexes: indexes - } - ); - - // check lenders lp balance - _assertLenderLpBalance( - { - lender: _lender1, - index: indexes[0], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: _lender2, - index: indexes[0], - lpBalance: 15_000 * 1e27, - depositTime: _startTime + 2 hours - } - ); - _assertLenderLpBalance( - { - lender: _lender1, - index: indexes[1], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: _lender2, - index: indexes[1], - lpBalance: 30_000 * 1e27, - depositTime: _startTime + 2 hours - } - ); - _assertLenderLpBalance( - { - lender: _lender1, - index: indexes[2], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: _lender2, - index: indexes[2], - lpBalance: 45_000 * 1e27, - depositTime: _startTime + 2 hours - } - ); - } -} diff --git a/tests/forge/ERC20Pool/ERC20PoolTransferLPs.t.sol b/tests/forge/ERC20Pool/ERC20PoolTransferLPs.t.sol new file mode 100644 index 000000000..7ac64107d --- /dev/null +++ b/tests/forge/ERC20Pool/ERC20PoolTransferLPs.t.sol @@ -0,0 +1,541 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.14; + +import { ERC20HelperContract } from './ERC20DSTestPlus.sol'; + +import 'src/libraries/helpers/PoolHelper.sol'; + +contract ERC20PoolTransferLPsTest is ERC20HelperContract { + + address internal _lender; + address internal _lender1; + address internal _lender2; + + function setUp() external { + _lender = makeAddr("lender"); + _lender1 = makeAddr("lender1"); + _lender2 = makeAddr("lender2"); + + _mintQuoteAndApproveTokens(_lender, 200_000 * 1e18); + _mintQuoteAndApproveTokens(_lender1, 200_000 * 1e18); + _mintQuoteAndApproveTokens(_lender2, 200_000 * 1e18); + } + + /**************************/ + /*** Transfer LPs Tests ***/ + /**************************/ + + function testTransferLPsToZeroAddress() external tearDown { + uint256[] memory indexes = new uint256[](3); + indexes[0] = 2550; + indexes[1] = 2551; + indexes[2] = 2552; + + // should fail if allowed owner is not set + _assertTransferNoAllowanceRevert({ + operator: _lender, + from: _lender1, + to: _lender2, + indexes: indexes + }); + + // should fail if allowed owner is set to 0x + changePrank(_lender1); + _pool.approveLpOwnership(address(0), indexes[0], 1_000 * 1e18); + + _assertTransferNoAllowanceRevert({ + operator: _lender, + from: _lender1, + to: _lender2, + indexes: indexes + }); + } + + function testTransferLPsToUnallowedAddress() external tearDown { + uint256[] memory indexes = new uint256[](3); + indexes[0] = 2550; + indexes[1] = 2551; + indexes[2] = 2552; + + // should fail if allowed owner is set to lender2 address but trying to transfer to lender address + changePrank(_lender1); + _pool.approveLpOwnership(_lender2, indexes[0], 1_000 * 1e18); + _pool.approveLpOwnership(_lender2, indexes[1], 1_000 * 1e18); + _pool.approveLpOwnership(_lender2, indexes[2], 1_000 * 1e18); + + _assertTransferNoAllowanceRevert({ + operator: _lender, + from: _lender1, + to: _lender2, + indexes: indexes + }); + } + + function testTransferLPsToInvalidIndex() external tearDown { + uint256[] memory indexes = new uint256[](3); + indexes[0] = 9999; + indexes[1] = 2550; + indexes[2] = 2552; + + // should fail since 9999 is not a valid index + changePrank(_lender1); + _pool.approveLpOwnership(_lender2, indexes[0], 1_000 * 1e18); + _pool.approveLpOwnership(_lender2, indexes[1], 1_000 * 1e18); + _pool.approveLpOwnership(_lender2, indexes[2], 1_000 * 1e18); + + _assertTransferInvalidIndexRevert({ + operator: _lender, + from: _lender1, + to: _lender2, + indexes: indexes + }); + } + + function testTransferLPsGreaterThanBalance() external tearDown { + uint256[] memory indexes = new uint256[](2); + indexes[0] = 2550; + indexes[1] = 2551; + + _addInitialLiquidity({ + from: _lender1, + amount: 10_000 * 1e18, + index: indexes[0] + }); + _addInitialLiquidity({ + from: _lender1, + amount: 20_000 * 1e18, + index: indexes[1] + }); + + // set allowed owner to lender2 address + _pool.approveLpOwnership(_lender2, indexes[0], 10_000 * 1e18); + _pool.approveLpOwnership(_lender2, indexes[1], 30_000 * 1e18); + + _assertTransferNoAllowanceRevert({ + operator: _lender2, + from: _lender1, + to: _lender2, + indexes: indexes + }); + } + + function testTransferLPsToSameOwner() external { + uint256[] memory indexes = new uint256[](1); + indexes[0] = 2550; + + skip(1 hours); + + _addInitialLiquidity({ + from: _lender1, + amount: 10_000 * 1e18, + index: indexes[0] + }); + + changePrank(_lender1); + _pool.approveLpOwnership(_lender1, indexes[0], 10_000 * 1e18); + + _assertLenderLpBalance({ + lender: _lender1, + index: indexes[0], + lpBalance: 10_000 * 1e18, + depositTime: _startTime + 1 hours + }); + + // should revert if trying to transfer LPs to same address + _assertTransferToSameOwnerRevert({ + operator: _lender, + from: _lender1, + to: _lender1, + indexes: indexes + }); + } + + function testTransferLPsForAllIndexes() external tearDown { + uint256[] memory indexes = new uint256[](3); + indexes[0] = 2550; + indexes[1] = 2551; + indexes[2] = 2552; + + skip(1 hours); + + _addInitialLiquidity({ + from: _lender1, + amount: 10_000 * 1e18, + index: indexes[0] + }); + _addInitialLiquidity({ + from: _lender1, + amount: 20_000 * 1e18, + index: indexes[1] + }); + _addInitialLiquidity({ + from: _lender1, + amount: 30_000 * 1e18, + index: indexes[2] + }); + + // check lenders lp balance + _assertLenderLpBalance({ + lender: _lender1, + index: indexes[0], + lpBalance: 10_000 * 1e18, + depositTime: _startTime + 1 hours + }); + _assertLenderLpBalance({ + lender: _lender2, + index: indexes[0], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: _lender1, + index: indexes[1], + lpBalance: 20_000 * 1e18, + depositTime: _startTime + 1 hours + }); + _assertLenderLpBalance({ + lender: _lender2, + index: indexes[1], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: _lender1, + index: indexes[2], + lpBalance: 30_000 * 1e18, + depositTime: _startTime + 1 hours + }); + _assertLenderLpBalance({ + lender: _lender2, + index: indexes[2], + lpBalance: 0, + depositTime: 0 + }); + + // set allowed owner to lender2 address + _pool.approveLpOwnership(_lender2, indexes[0], 10_000 * 1e18); + _pool.approveLpOwnership(_lender2, indexes[1], 20_000 * 1e18); + _pool.approveLpOwnership(_lender2, indexes[2], 30_000 * 1e18); + + // transfer LP tokens for all indexes + _transferLPs({ + operator: _lender, + from: _lender1, + to: _lender2, + indexes: indexes, + lpBalance: 60_000 * 1e18 + }); + + // check that old token ownership was removed - a new transfer should fail + _assertTransferNoAllowanceRevert({ + operator: _lender, + from: _lender1, + to: _lender2, + indexes: indexes + }); + + // check lenders lp balance + _assertLenderLpBalance({ + lender: _lender1, + index: indexes[0], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: _lender2, + index: indexes[0], + lpBalance: 10_000 * 1e18, + depositTime: _startTime + 1 hours + }); + _assertLenderLpBalance({ + lender: _lender1, + index: indexes[1], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: _lender2, + index: indexes[1], + lpBalance: 20_000 * 1e18, + depositTime: _startTime + 1 hours + }); + _assertLenderLpBalance({ + lender: _lender1, + index: indexes[2], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: _lender2, + index: indexes[2], + lpBalance: 30_000 * 1e18, + depositTime: _startTime + 1 hours + }); + } + + function testTransferLPsForTwoIndexes() external tearDown { + uint256[] memory depositIndexes = new uint256[](3); + depositIndexes[0] = 2550; + depositIndexes[1] = 2551; + depositIndexes[2] = 2552; + + uint256[] memory transferIndexes = new uint256[](2); + transferIndexes[0] = 2550; + transferIndexes[1] = 2552; + + _addInitialLiquidity({ + from: _lender1, + amount: 10_000 * 1e18, + index: depositIndexes[0] + }); + _addInitialLiquidity({ + from: _lender1, + amount: 20_000 * 1e18, + index: depositIndexes[1] + }); + _addInitialLiquidity({ + from: _lender1, + amount: 30_000 * 1e18, + index: depositIndexes[2] + }); + + // check lenders lp balance + _assertLenderLpBalance({ + lender: _lender1, + index: depositIndexes[0], + lpBalance: 10_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: _lender2, + index: depositIndexes[0], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: _lender1, + index: depositIndexes[1], + lpBalance: 20_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: _lender2, + index: depositIndexes[1], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: _lender1, + index: depositIndexes[2], + lpBalance: 30_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: _lender2, + index: depositIndexes[2], + lpBalance: 0, + depositTime: 0 + }); + + // set allowed owner to lender2 address + _pool.approveLpOwnership(_lender2, transferIndexes[0], 10_000 * 1e18); + _pool.approveLpOwnership(_lender2, transferIndexes[1], 30_000 * 1e18); + + // transfer LP tokens for 2 indexes + _transferLPs({ + operator: _lender, + from: _lender1, + to: _lender2, + indexes: transferIndexes, + lpBalance: 40_000 * 1e18 + }); + + // check that old token ownership was removed - transfer with same indexes should fail + _assertTransferNoAllowanceRevert({ + operator: _lender, + from: _lender1, + to: _lender2, + indexes: transferIndexes + }); + + // check lenders lp balance + _assertLenderLpBalance({ + lender: _lender1, + index: depositIndexes[0], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: _lender2, + index: depositIndexes[0], + lpBalance: 10_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: _lender1, + index: depositIndexes[1], + lpBalance: 20_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: _lender2, + index: depositIndexes[1], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: _lender1, + index: depositIndexes[2], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: _lender2, + index: depositIndexes[2], + lpBalance: 30_000 * 1e18, + depositTime: _startTime + }); + } + + function testTransferLPsToLenderWithLPTokens() external tearDown { + uint256[] memory indexes = new uint256[](3); + indexes[0] = 2550; + indexes[1] = 2551; + indexes[2] = 2552; + + skip(1 hours); + + _addInitialLiquidity({ + from: _lender1, + amount: 10_000 * 1e18, + index: indexes[0] + }); + _addInitialLiquidity({ + from: _lender1, + amount: 20_000 * 1e18, + index: indexes[1] + }); + _addInitialLiquidity({ + from: _lender1, + amount: 30_000 * 1e18, + index: indexes[2] + }); + + skip(1 hours); + + _addInitialLiquidity({ + from: _lender2, + amount: 5_000 * 1e18, + index: indexes[0] + }); + _addInitialLiquidity({ + from: _lender2, + amount: 10_000 * 1e18, + index: indexes[1] + }); + _addInitialLiquidity({ + from: _lender2, + amount: 15_000 * 1e18, + index: indexes[2] + }); + + // check lenders lp balance + _assertLenderLpBalance({ + lender: _lender1, + index: indexes[0], + lpBalance: 10_000 * 1e18, + depositTime: _startTime + 1 hours + }); + _assertLenderLpBalance({ + lender: _lender2, + index: indexes[0], + lpBalance: 5_000 * 1e18, + depositTime: _startTime + 2 hours + }); + _assertLenderLpBalance({ + lender: _lender1, + index: indexes[1], + lpBalance: 20_000 * 1e18, + depositTime: _startTime + 1 hours + }); + _assertLenderLpBalance({ + lender: _lender2, + index: indexes[1], + lpBalance: 10_000 * 1e18, + depositTime: _startTime + 2 hours + }); + _assertLenderLpBalance({ + lender: _lender1, + index: indexes[2], + lpBalance: 30_000 * 1e18, + depositTime: _startTime + 1 hours + }); + _assertLenderLpBalance({ + lender: _lender2, + index: indexes[2], + lpBalance: 15_000 * 1e18, + depositTime: _startTime + 2 hours + }); + + // set allowed owner to lender2 address + changePrank(_lender1); + _pool.approveLpOwnership(_lender2, indexes[0], 10_000 * 1e18); + _pool.approveLpOwnership(_lender2, indexes[1], 20_000 * 1e18); + _pool.approveLpOwnership(_lender2, indexes[2], 30_000 * 1e18); + + // transfer LP tokens for all indexes + _transferLPs({ + operator: _lender, + from: _lender1, + to: _lender2, + indexes: indexes, + lpBalance: 60_000 * 1e18 + }); + + // check that old token ownership was removed - transfer with same indexes should fail + _assertTransferNoAllowanceRevert({ + operator: _lender, + from: _lender1, + to: _lender2, + indexes: indexes + }); + + // check lenders lp balance + _assertLenderLpBalance({ + lender: _lender1, + index: indexes[0], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: _lender2, + index: indexes[0], + lpBalance: 15_000 * 1e18, + depositTime: _startTime + 2 hours + }); + _assertLenderLpBalance({ + lender: _lender1, + index: indexes[1], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: _lender2, + index: indexes[1], + lpBalance: 30_000 * 1e18, + depositTime: _startTime + 2 hours + }); + _assertLenderLpBalance({ + lender: _lender1, + index: indexes[2], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: _lender2, + index: indexes[2], + lpBalance: 45_000 * 1e18, + depositTime: _startTime + 2 hours + }); + } +} diff --git a/tests/forge/ERC20Pool/ERC20SafeTransferTokens.sol b/tests/forge/ERC20Pool/ERC20SafeTransferTokens.sol index d72edb38e..06abe35ff 100644 --- a/tests/forge/ERC20Pool/ERC20SafeTransferTokens.sol +++ b/tests/forge/ERC20Pool/ERC20SafeTransferTokens.sol @@ -36,7 +36,7 @@ contract ERC20SafeTransferTokens is ERC20HelperContract { usdt.approve(address(pool), 1_000 * 1e18); - pool.addQuoteToken(1_000 * 1e18, 2549); + pool.addQuoteToken(1_000 * 1e18, 2549, type(uint256).max); changePrank(_borrower); @@ -54,7 +54,7 @@ contract ERC20SafeTransferTokens is ERC20HelperContract { _quote.approve(address(pool), 1_000 * 1e18); - pool.addQuoteToken(1_000 * 1e18, 2549); + pool.addQuoteToken(1_000 * 1e18, 2549, type(uint256).max); changePrank(_borrower); 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..84edad7fb --- /dev/null +++ b/tests/forge/ERC20Pool/invariants/handlers/BasicPoolHandler.sol @@ -0,0 +1,359 @@ +// 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()")) || + err == keccak256(abi.encodeWithSignature("AuctionActive()")) + ); + } + } + + 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()")) || + err == keccak256(abi.encodeWithSignature("AuctionActive()")) + ); + } + } + + 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..29c6b6076 --- /dev/null +++ b/tests/forge/ERC20Pool/regression/RegressionTestLiquidation.t.sol @@ -0,0 +1,70 @@ +// 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(); + } + + function test_incorrect_no_of_borrowers() public { + _liquidationPoolHandler.moveQuoteToken(18178450611611937161732340858718395124120481640398450530303803, 0, 93537843531612826457318744802930982491, 15596313608676556633725998020226886686244513); + _liquidationPoolHandler.addCollateral(2208149704044082902772911545020934265, 340235628931125711729099234105522626267587665393753030264689924088, 2997844437211835697043096396926932785920355866486893005710984415271); + _liquidationPoolHandler.moveQuoteToken(56944009718062971164908977784993293, 737882204379007468599822110965749781465, 1488100463155679769353095066686506252, 11960033727528802202227468733333727294); + _liquidationPoolHandler.moveQuoteToken(47205392335275917691737183012282140599753693978176314740917, 2, 115792089237316195423570985008687907853269984665640564039457584007913129639934, 164043848691337333691028718232); + _liquidationPoolHandler.kickAuction(184206711567329609153924955630229148705869686378631519380021040314, 78351, 115792089237316195423570985008687907853269984665640564039457584007913129639933); + _liquidationPoolHandler.kickAuction(3, 199726916764352560035199423206927461876998880387108455962754538835220966553, 3); + _liquidationPoolHandler.removeQuoteToken(999999991828440064944955196599190431639924811, 2781559202773230142346489450532860130, 3000000005240421579956496007310960085855569344); + _liquidationPoolHandler.pullCollateral(48768502867710912107594904694036421700, 275047566877984818806178837359260100); + _liquidationPoolHandler.bucketTake(2, 115792089237316195423570985008687907853269984665640564039457584007913129639934, false, 8154570107391684241724530527782571978369827827856399749867491880); + _liquidationPoolHandler.removeCollateral(43733538637150108518954934566131291302796656384802361118757432084573, 1, 115792089237316195423570985008687907853269984665640564039457584007913129639934); + _liquidationPoolHandler.addQuoteToken(1, 2, 2); + _liquidationPoolHandler.repayDebt(647805461526201272, 0); + _liquidationPoolHandler.kickAuction(1019259585194528028904148545812353964867041444572537077023497678982801, 58796345025472936970320, 131319002678489819637546489086162345032717166507611595521); + _liquidationPoolHandler.moveQuoteToken(2, 2, 0, 115792089237316195423570985008687907853269984665640564039457584007913129639935); + _liquidationPoolHandler.moveQuoteToken(6164937621056362865643346803975636714, 4, 115792089237316195423570985008687907853269984665640564039457584007913129639934, 315548939052682258); + _liquidationPoolHandler.repayDebt(2987067394366841692658, 170206016570563384086766968869520628); + _liquidationPoolHandler.pledgeCollateral(3558446182295495994762049031, 0); + _liquidationPoolHandler.drawDebt(4525700839008283200312069904720925039, 3000000000753374912785563581177665475703155339); + _liquidationPoolHandler.kickAuction(1, 3559779948348618822016735773117619950447774, 218801416747720); + _liquidationPoolHandler.addQuoteToken(1469716416900282992357252011629715552, 13037214114647887147246343731476169800, 984665637618013480616943810604306792); + _liquidationPoolHandler.pullCollateral(438961419917818200942534689247815826455600131, 64633474453314038763068322072915580384442279897841981); + + invariant_auctions_A3_A4(); + } +} \ 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 diff --git a/tests/forge/ERC721Pool/ERC721DSTestPlus.sol b/tests/forge/ERC721Pool/ERC721DSTestPlus.sol index 6d8bc97a8..964941451 100644 --- a/tests/forge/ERC721Pool/ERC721DSTestPlus.sol +++ b/tests/forge/ERC721Pool/ERC721DSTestPlus.sol @@ -5,8 +5,8 @@ import { ERC20 } from '@openzeppelin/contracts/token/ERC20/ERC20.sol'; import '@openzeppelin/contracts/token/ERC721/ERC721.sol'; import '@openzeppelin/contracts/utils/structs/EnumerableSet.sol'; -import { DSTestPlus } from '../utils/DSTestPlus.sol'; -import { NFTCollateralToken, Token } from '../utils/Tokens.sol'; +import { DSTestPlus } from '../utils/DSTestPlus.sol'; +import { NFTCollateralToken, Token, TokenWithNDecimals } from '../utils/Tokens.sol'; import { ERC721Pool } from 'src/ERC721Pool.sol'; import { ERC721PoolFactory } from 'src/ERC721PoolFactory.sol'; @@ -25,7 +25,7 @@ abstract contract ERC721DSTestPlus is DSTestPlus, IERC721PoolEvents { using EnumerableSet for EnumerableSet.UintSet; NFTCollateralToken internal _collateral; - Token internal _quote; + TokenWithNDecimals internal _quote; ERC20 internal _ajnaToken; mapping(uint256 => uint256) NFTidToIndex; @@ -100,13 +100,12 @@ abstract contract ERC721DSTestPlus is DSTestPlus, IERC721PoolEvents { { uint256 fractionOfNftRemaining = lpsAsCollateral % 1e18; assertLt(fractionOfNftRemaining, 1e18); - - // 1 wei workaround due to rounding - depositRequired = Maths.wmul(1e18 - fractionOfNftRemaining + 1, price); + + depositRequired = Maths.wmul(1e18 - fractionOfNftRemaining, price); } deal(_pool.quoteTokenAddress(), lender, depositRequired); Token(_pool.quoteTokenAddress()).approve(address(_pool) , depositRequired); - _pool.addQuoteToken(depositRequired, bucketIndex); + _pool.addQuoteToken(depositRequired, bucketIndex, block.timestamp + 1 minutes); (lenderLpBalance, ) = _pool.lenderInfo(bucketIndex, lender); lpsAsCollateral = _poolUtils.lpsToCollateral(address(_pool), lenderLpBalance, bucketIndex); } @@ -175,7 +174,7 @@ abstract contract ERC721DSTestPlus is DSTestPlus, IERC721PoolEvents { emit Transfer(from, address(_pool), tokenIds[i]); } - lps_ = ERC721Pool(address(_pool)).addCollateral(tokenIds, index); + lps_ = ERC721Pool(address(_pool)).addCollateral(tokenIds, index, block.timestamp + 10 minutes); for (uint256 i = 0; i < tokenIds.length; i++) { assertEq(_collateral.ownerOf(tokenIds[i]), address(_pool)); // token is owned by pool after add @@ -336,7 +335,7 @@ abstract contract ERC721DSTestPlus is DSTestPlus, IERC721PoolEvents { emit RepayDebt(borrower, amountRepaid, collateralToPull, newLup); } - ERC721Pool(address(_pool)).repayDebt(borrower, amountToRepay, collateralToPull); + ERC721Pool(address(_pool)).repayDebt(borrower, amountToRepay, collateralToPull, borrower, MAX_FENWICK_INDEX); // post pull checks if (collateralToPull != 0) { @@ -352,7 +351,7 @@ abstract contract ERC721DSTestPlus is DSTestPlus, IERC721PoolEvents { emit RepayDebt(borrower, amountRepaid, collateralToPull, newLup); } - ERC721Pool(address(_pool)).repayDebt(borrower, amountToRepay, collateralToPull); + ERC721Pool(address(_pool)).repayDebt(borrower, amountToRepay, collateralToPull, borrower, MAX_FENWICK_INDEX); } } @@ -430,10 +429,42 @@ abstract contract ERC721DSTestPlus is DSTestPlus, IERC721PoolEvents { } } + function _assertCollateralInvariants() internal { + uint256 collateralInBuckets; + for(uint256 bucketIndex = 0; bucketIndex <= 7388; bucketIndex++) { + (, uint256 bucketCollateral, , , ) = _pool.bucketInfo(bucketIndex); + collateralInBuckets += bucketCollateral; + } + + uint256 borrowersCollateral; + for (uint256 i = 0; i < borrowers.length(); i++) { + (, uint256 borrowerCollateral, ) = _poolUtils.borrowerInfo(address(_pool), borrowers.at(i)); + borrowersCollateral += borrowerCollateral; + } + + // pool pledged collateral accumulator should be equal with the amounts of collateral owned by borrowers + assertEq(borrowersCollateral, _pool.pledgedCollateral()); + + // collateral in buckets + collateral owned borrowers should be equal with the total number of tokens owned by the pool + uint256 poolBalance = _collateral.balanceOf(address(_pool)); + assertEq(collateralInBuckets + borrowersCollateral, poolBalance * 1e18); + } + /**********************/ /*** Revert asserts ***/ /**********************/ + function _assertAddCollateralExpiredRevert( + address from, + uint256[] memory tokenIds, + uint256 index, + uint256 expiry + ) internal { + changePrank(from); + vm.expectRevert(IPoolErrors.TransactionExpired.selector); + ERC721Pool(address(_pool)).addCollateral(tokenIds, index, expiry); + } + function _assertDeployWith0xAddressRevert( address poolFactory, address collateral, @@ -504,11 +535,22 @@ abstract contract ERC721DSTestPlus is DSTestPlus, IERC721PoolEvents { uint256 indexLimit ) internal override { changePrank(from); - vm.expectRevert(IPoolErrors.LimitIndexReached.selector); + vm.expectRevert(IPoolErrors.LimitIndexExceeded.selector); uint256[] memory emptyArray; ERC721Pool(address(_pool)).drawDebt(from, amount, indexLimit, emptyArray); } + function _assertMergeRemoveCollateralAuctionNotClearedRevert( + address from, + uint256 toIndex, + uint256 noOfNFTsToRemove, + uint256[] memory removeCollateralAtIndex + ) internal { + changePrank(from); + vm.expectRevert(abi.encodeWithSignature('AuctionNotCleared()')); + ERC721Pool(address(_pool)).mergeOrRemoveCollateral(removeCollateralAtIndex, noOfNFTsToRemove, toIndex); + } + function _assertCannotMergeToHigherPriceRevert( address from, uint256 toIndex, @@ -531,6 +573,17 @@ abstract contract ERC721DSTestPlus is DSTestPlus, IERC721PoolEvents { ERC721Pool(address(_pool)).drawDebt(from, amount, indexLimit, emptyArray); } + function _assertBorrowDustRevert( + address from, + uint256 amount, + uint256 indexLimit + ) internal { + changePrank(from); + vm.expectRevert(IPoolErrors.DustAmountNotExceeded.selector); + uint256[] memory emptyArray; + ERC721Pool(address(_pool)).drawDebt(from, amount, indexLimit, emptyArray); + } + function _assertBorrowMinDebtRevert( address from, uint256 amount, @@ -548,7 +601,17 @@ abstract contract ERC721DSTestPlus is DSTestPlus, IERC721PoolEvents { ) internal { changePrank(from); vm.expectRevert(IPoolErrors.InsufficientCollateral.selector); - ERC721Pool(address(_pool)).repayDebt(from, 0, amount); + ERC721Pool(address(_pool)).repayDebt(from, 0, amount, from, MAX_FENWICK_INDEX); + } + + function _assertPullLimitIndexRevert( + address from, + uint256 amount, + uint256 indexLimit + ) internal { + changePrank(from); + vm.expectRevert(IPoolErrors.LimitIndexExceeded.selector); + ERC721Pool(address(_pool)).repayDebt(from, 0, amount, from, indexLimit); } function _assertRepayNoDebtRevert( @@ -558,7 +621,7 @@ abstract contract ERC721DSTestPlus is DSTestPlus, IERC721PoolEvents { ) internal { changePrank(from); vm.expectRevert(IPoolErrors.NoDebt.selector); - ERC721Pool(address(_pool)).repayDebt(borrower, amount, 0); + ERC721Pool(address(_pool)).repayDebt(borrower, amount, 0, borrower, MAX_FENWICK_INDEX); } function _assertRepayMinDebtRevert( @@ -568,7 +631,7 @@ abstract contract ERC721DSTestPlus is DSTestPlus, IERC721PoolEvents { ) internal { changePrank(from); vm.expectRevert(IPoolErrors.AmountLTMinDebt.selector); - ERC721Pool(address(_pool)).repayDebt(borrower, amount, 0); + ERC721Pool(address(_pool)).repayDebt(borrower, amount, 0, borrower, MAX_FENWICK_INDEX); } function _assertRemoveCollateralNoClaimRevert( @@ -603,7 +666,7 @@ abstract contract ERC721HelperContract is ERC721DSTestPlus { _collateral = new NFTCollateralToken(); vm.makePersistent(address(_collateral)); - _quote = new Token("Quote", "Q"); + _quote = new TokenWithNDecimals("Quote", "Q", 18); vm.makePersistent(address(_quote)); _ajnaToken = ERC20(_ajna); vm.makePersistent(_ajna); @@ -670,6 +733,65 @@ abstract contract ERC721HelperContract is ERC721DSTestPlus { } } +abstract contract ERC721NDecimalsHelperContract is ERC721DSTestPlus { + using EnumerableSet for EnumerableSet.AddressSet; + ERC721PoolFactory internal _poolFactory; + + constructor(uint8 decimals) { + vm.createSelectFork(vm.envString("ETH_RPC_URL")); + + _collateral = new NFTCollateralToken(); + vm.makePersistent(address(_collateral)); + _quote = new TokenWithNDecimals("Quote", "Q", decimals); + vm.makePersistent(address(_quote)); + _ajnaToken = ERC20(_ajna); + vm.makePersistent(_ajna); + _poolUtils = new PoolInfoUtils(); + vm.makePersistent(address(_poolUtils)); + _poolFactory = new ERC721PoolFactory(_ajna); + vm.makePersistent(address(_poolFactory)); + + _startTime = block.timestamp; + uint256[] memory tokenIds; + address contractAddress = _poolFactory.deployPool(address(_collateral), address(_quote), tokenIds, 0.05 * 10**18); + vm.makePersistent(contractAddress); + _pool = ERC721Pool(contractAddress); + } + + function _mintAndApproveQuoteTokens(address operator_, uint256 mintAmount_) internal { + deal(address(_quote), operator_, mintAmount_); + vm.prank(operator_); + _quote.approve(address(_pool), type(uint256).max); + } + + function _mintAndApproveCollateralTokens(address operator_, uint256 mintAmount_) internal { + _collateral.mint(operator_, mintAmount_); + vm.prank(operator_); + _collateral.setApprovalForAll(address(_pool), true); + } + + /** + * @dev Creates debt for an anonymous non-player borrower not otherwise involved in the test. + **/ + function _anonBorrowerDrawsDebt(uint256 loanAmount) internal { + // _anonBorrowerCount += 1; + + address borrower = makeAddr(string(abi.encodePacked("anonBorrower", borrowers.length()))); + vm.stopPrank(); + _mintAndApproveCollateralTokens(borrower, 1); + uint256[] memory tokenIdsToAdd = new uint256[](1); + tokenIdsToAdd[0] = _collateral.totalSupply(); + + _drawDebtNoLupCheck({ + from: borrower, + borrower: borrower, + amountToBorrow: loanAmount, + limitIndex: MAX_FENWICK_INDEX, + tokenIds: tokenIdsToAdd + }); + } +} + abstract contract ERC721FuzzyHelperContract is ERC721DSTestPlus { uint256 public constant LARGEST_AMOUNT = type(uint256).max / 10**27; @@ -678,7 +800,7 @@ abstract contract ERC721FuzzyHelperContract is ERC721DSTestPlus { constructor() { _collateral = new NFTCollateralToken(); - _quote = new Token("Quote", "Q"); + _quote = new TokenWithNDecimals("Quote", "Q", 18); _ajnaToken = ERC20(_ajna); _poolUtils = new PoolInfoUtils(); _poolFactory = new ERC721PoolFactory(_ajna); diff --git a/tests/forge/ERC721Pool/ERC721NonStandardTokenTransfer.sol b/tests/forge/ERC721Pool/ERC721NonStandardTokenTransfer.sol deleted file mode 100644 index 82c16794b..000000000 --- a/tests/forge/ERC721Pool/ERC721NonStandardTokenTransfer.sol +++ /dev/null @@ -1,121 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.14; - -import { ERC721HelperContract } from './ERC721DSTestPlus.sol'; - -import 'src/ERC721Pool.sol'; -import 'src/ERC721PoolFactory.sol'; - -interface ICryptoFighters { - function transferFrom(address from_, address to_, uint256 tokenId_) external; - function transfer(address to_, uint256 tokenId_) external; - function approve(address to_, uint256 tokenId_) external; - function ownerOf(uint256 tokenId_) external returns(address); - function fighterIndexToApproved(uint256 tokenId_) external returns(address); -} - -contract ERC721PoolNonStandardNftTest is ERC721HelperContract { - address internal _borrower; - address cryptoKittiesAddress = 0x06012c8cf97BEaD5deAe237070F9587f8E7A266d; - ICryptoKitties cryptoKittiesContract = ICryptoKitties(cryptoKittiesAddress); - address cryptoFightersAddress = 0x87d598064c736dd0C712D329aFCFAA0Ccc1921A1; - ICryptoFighters cryptoFightersContract = ICryptoFighters(cryptoFightersAddress); - address cryptoPunksAddress = 0xb47e3cd837dDF8e4c57F05d70Ab865de6e193BBB; - ICryptoPunks cryptoPunksContract = ICryptoPunks(cryptoPunksAddress); - - function setUp() external { - // Borrower has Crypto Kitties/Fighters/Punks in his wallet at specified block - vm.createSelectFork(vm.envString("ETH_RPC_URL"), 16167859); - } - - function testCryptoKittiesNftTransfer() external { - uint256[] memory tokenIds; - _pool = ERC721Pool(new ERC721PoolFactory(_ajna).deployPool(cryptoKittiesAddress, address(_quote), tokenIds, 0.05 * 10**18)); - - _borrower = 0xDAd45d13472568181B83A731D1e170f4c6e95a83; - changePrank(_borrower); - - assertEq(cryptoKittiesContract.ownerOf(1777317), _borrower); - // Borrower approves Nft - cryptoKittiesContract.approve(address(_pool), 1777317); - - assertEq(cryptoKittiesContract.kittyIndexToApproved(1777317), address(_pool)); - - tokenIds = new uint256[](1); - tokenIds[0] = 1777317; - - // Pledge Collateral - ERC721Pool(address(_pool)).drawDebt(_borrower, 0, 0, tokenIds); - - // Check Pool is owner of NFT - assertEq(cryptoKittiesContract.ownerOf(1777317), address(_pool)); - - // Pull collateral - ERC721Pool(address(_pool)).repayDebt(_borrower, 0, 1); - - // Check Borrower is owner of NFT - assertEq(cryptoKittiesContract.ownerOf(1777317), _borrower); - - } - - function testCryptoFightersNftTransfer() external { - uint256[] memory tokenIds; - _pool = ERC721Pool(new ERC721PoolFactory(_ajna).deployPool(cryptoFightersAddress, address(_quote), tokenIds, 0.05 * 10**18)); - - _borrower = 0x4E2EAE6ABA4E61Eb16c2D7905a4747323Ca7a504; - changePrank(_borrower); - - assertEq(cryptoFightersContract.ownerOf(1), _borrower); - // Borrower approves Nft - cryptoFightersContract.approve(address(_pool), 1); - - assertEq(cryptoFightersContract.fighterIndexToApproved(1), address(_pool)); - - tokenIds = new uint256[](1); - tokenIds[0] = 1; - - // Pledge Collateral - ERC721Pool(address(_pool)).drawDebt(_borrower, 0, 0, tokenIds); - - // Check Pool is owner of NFT - assertEq(cryptoFightersContract.ownerOf(1), address(_pool)); - - // Pull collateral - ERC721Pool(address(_pool)).repayDebt(_borrower, 0, 1); - - // Check Borrower is owner of NFT - assertEq(cryptoFightersContract.ownerOf(1), _borrower); - - } - - function testCryptoPunksNftTransfer() external { - uint256[] memory tokenIds; - _pool = ERC721Pool(new ERC721PoolFactory(_ajna).deployPool(cryptoPunksAddress, address(_quote), tokenIds, 0.05 * 10**18)); - - - _borrower = 0xB88F61E6FbdA83fbfffAbE364112137480398018; - changePrank(_borrower); - - // Check Borrower is owner of NFT - assertEq(cryptoPunksContract.punkIndexToAddress(1), _borrower); - - // Borrower approves Nft - cryptoPunksContract.offerPunkForSaleToAddress(1, 0, address(_pool)); - - tokenIds = new uint256[](1); - tokenIds[0] = 1; - - // Pledge Collateral - ERC721Pool(address(_pool)).drawDebt(_borrower, 0, 0, tokenIds); - - // Check Pool is owner of NFT - assertEq(cryptoPunksContract.punkIndexToAddress(1), address(_pool)); - - // Pull collateral - ERC721Pool(address(_pool)).repayDebt(_borrower, 0, 1); - - // Check Borrower is owner of NFT - assertEq(cryptoPunksContract.punkIndexToAddress(1), _borrower); - - } -} diff --git a/tests/forge/ERC721Pool/ERC721PoolBorrow.t.sol b/tests/forge/ERC721Pool/ERC721PoolBorrow.t.sol index 806ffc285..04ae37064 100644 --- a/tests/forge/ERC721Pool/ERC721PoolBorrow.t.sol +++ b/tests/forge/ERC721Pool/ERC721PoolBorrow.t.sol @@ -1,13 +1,13 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.14; -import { ERC721HelperContract, ERC721FuzzyHelperContract } from './ERC721DSTestPlus.sol'; +import { ERC721HelperContract, ERC721FuzzyHelperContract, ERC721NDecimalsHelperContract } from './ERC721DSTestPlus.sol'; import 'src/ERC721Pool.sol'; import 'src/libraries/internal/Maths.sol'; -import { MAX_PRICE, _priceAt } from 'src/libraries/helpers/PoolHelper.sol'; +import { MAX_FENWICK_INDEX, MAX_PRICE, _priceAt } from 'src/libraries/helpers/PoolHelper.sol'; abstract contract ERC721PoolBorrowTest is ERC721HelperContract { address internal _borrower; @@ -59,127 +59,101 @@ contract ERC721SubsetPoolBorrowTest is ERC721PoolBorrowTest { function testBorrowLimitReached() external tearDown { // lender deposits 10000 Quote into 3 buckets - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 2550 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 2551 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 2552 - } - ); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2550 + }); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2551 + }); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2552 + }); // borrower deposits three NFTs into the subset pool uint256[] memory tokenIdsToAdd = new uint256[](3); tokenIdsToAdd[0] = 1; tokenIdsToAdd[1] = 3; tokenIdsToAdd[2] = 5; - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - tokenIds: tokenIdsToAdd - } - ); + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + tokenIds: tokenIdsToAdd + }); // should revert if insufficient quote available before limit price - _assertBorrowLimitIndexRevert( - { - from: _borrower, - amount: 21_000 * 1e18, - indexLimit: 2551 - } - ); + _assertBorrowLimitIndexRevert({ + from: _borrower, + amount: 21_000 * 1e18, + indexLimit: 2551 + }); } function testBorrowBorrowerUnderCollateralized() external tearDown { // add initial quote to the pool - _addInitialLiquidity( - { - from: _lender, - amount: 1_000 * 1e18, - index: 3575 - } - ); + _addInitialLiquidity({ + from: _lender, + amount: 1_000 * 1e18, + index: 3575 + }); // borrower pledges some collateral uint256[] memory tokenIdsToAdd = new uint256[](2); tokenIdsToAdd[0] = 5; tokenIdsToAdd[1] = 3; - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - tokenIds: tokenIdsToAdd - } - ); + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + tokenIds: tokenIdsToAdd + }); // should revert if borrower did not deposit enough collateral - _assertBorrowBorrowerUnderCollateralizedRevert( - { - from: _borrower, - amount: 40 * 1e18, - indexLimit: 4000 - } - ); + _assertBorrowBorrowerUnderCollateralizedRevert({ + from: _borrower, + amount: 40 * 1e18, + indexLimit: 4000 + }); } function testBorrowPoolUnderCollateralized() external tearDown { // add initial quote to the pool - _addInitialLiquidity( - { - from: _lender, - amount: 1_000 * 1e18, - index: 3232 - } - ); + _addInitialLiquidity({ + from: _lender, + amount: 1_000 * 1e18, + index: 3232 + }); // should revert if borrow would result in pool under collateralization - _assertBorrowBorrowerUnderCollateralizedRevert( - { - from: _borrower, - amount: 500, - indexLimit: 4000 - } - ); + _assertBorrowBorrowerUnderCollateralizedRevert({ + from: _borrower, + amount: 500, + indexLimit: 4000 + }); } function testBorrowAndRepay() external { // lender deposits 10000 Quote into 3 buckets - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 2550 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 2551 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 2552 - } - ); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2550 + }); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2551 + }); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2552 + }); // check initial token balances assertEq(_collateral.balanceOf(_borrower), 52); @@ -206,40 +180,33 @@ contract ERC721SubsetPoolBorrowTest is ERC721PoolBorrowTest { interestRateUpdate: _startTime }) ); - // check initial bucket state - _assertBucket( - { - index: 2550, - lpBalance: 10_000 * 1e27, - collateral: 0, - deposit: 10_000 * 1e18, - exchangeRate: 1 * 1e27 - } - ); + _assertBucket({ + index: 2550, + lpBalance: 10_000 * 1e18, + collateral: 0, + deposit: 10_000 * 1e18, + exchangeRate: 1 * 1e18 + }); // borrower deposits three NFTs into the subset pool uint256[] memory tokenIdsToAdd = new uint256[](3); tokenIdsToAdd[0] = 1; tokenIdsToAdd[1] = 3; tokenIdsToAdd[2] = 5; - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - tokenIds: tokenIdsToAdd - } - ); + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + tokenIds: tokenIdsToAdd + }); // borrower borrows from the pool uint256 borrowAmount = 3_000 * 1e18; - _borrow( - { - from: _borrower, - amount: borrowAmount, - indexLimit: 2551, - newLup: _priceAt(2550) - } - ); + _borrow({ + from: _borrower, + amount: borrowAmount, + indexLimit: 2551, + newLup: _priceAt(2550) + }); // check token balances after borrow assertEq(_collateral.balanceOf(_borrower), 49); @@ -266,28 +233,22 @@ contract ERC721SubsetPoolBorrowTest is ERC721PoolBorrowTest { interestRateUpdate: _startTime }) ); - // check bucket state after borrow - _assertBucket( - { - index: 2550, - lpBalance: 10_000 * 1e27, - collateral: 0, - deposit: 10_000 * 1e18, - exchangeRate: 1 * 1e27 - } - ); - + _assertBucket({ + index: 2550, + lpBalance: 10_000 * 1e18, + collateral: 0, + deposit: 10_000 * 1e18, + exchangeRate: 1 * 1e18 + }); // check borrower info after borrow - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 3_002.884615384615386000 * 1e18, - borrowerCollateral: 3 * 1e18, - borrowert0Np: 1_051.009615384615385100 * 1e18, - borrowerCollateralization: 3.007999714779824033 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 3_002.884615384615386000 * 1e18, + borrowerCollateral: 3 * 1e18, + borrowert0Np: 1_051.009615384615385100 * 1e18, + borrowerCollateralization: 3.007999714779824033 * 1e18 + }); // pass time to allow interest to accumulate skip(10 days); @@ -328,40 +289,33 @@ contract ERC721SubsetPoolBorrowTest is ERC721PoolBorrowTest { }) ); // check bucket state after partial repay - _assertBucket( - { - index: 2550, - lpBalance: 10_000 * 1e27, - collateral: 0, - deposit: 10_001.17341179741568 * 1e18, - exchangeRate: 1.000117341179741568 * 1e27 - } - ); - + _assertBucket({ + index: 2550, + lpBalance: 10_000 * 1e18, + collateral: 0, + deposit: 10_001.17341179741568 * 1e18, + exchangeRate: 1.000117341179741568 * 1e18 + }); // check borrower info after partial repay - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 1_507.000974734143274062 * 1e18, - borrowerCollateral: 3 * 1e18, - borrowert0Np: 1_051.009615384615385100 * 1e18, - borrowerCollateralization: 5.993809040625961846 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 1_507.000974734143274062 * 1e18, + borrowerCollateral: 3 * 1e18, + borrowert0Np: 1_051.009615384615385100 * 1e18, + borrowerCollateralization: 5.993809040625961846 * 1e18 + }); // pass time to allow additional interest to accumulate skip(10 days); // find pending debt after interest accumulation - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 1_508.860066921599065131 * 1e18, - borrowerCollateral: 3 * 1e18, - borrowert0Np: 1_051.009615384615385100 * 1e18, - borrowerCollateralization: 5.986423966420065589 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 1_508.860066921599065131 * 1e18, + borrowerCollateral: 3 * 1e18, + borrowert0Np: 1_051.009615384615385100 * 1e18, + borrowerCollateralization: 5.986423966420065589 * 1e18 + }); // mint additional quote to allow borrower to repay their loan plus interest deal(address(_quote), _borrower, _quote.balanceOf(_borrower) + 1_000 * 1e18); @@ -409,33 +363,26 @@ contract ERC721SubsetPoolBorrowTest is ERC721PoolBorrowTest { interestRateUpdate: _startTime + 20 days }) ); - _assertEMAs( - { - debtEma: 142.074529848655991542 * 1e18, - lupColEma: 851.567601449557349751 * 1e18 - } - ); - + _assertEMAs({ + debtEma: 142.074529848655991542 * 1e18, + lupColEma: 851.567601449557349751 * 1e18 + }); // check bucket state after fully repay - _assertBucket( - { - index: 2550, - lpBalance: 10_000 * 1e27, - collateral: 0, - deposit: 10_001.70173768409813 * 1e18, - exchangeRate: 1.000170173768409813 * 1e27 - } - ); + _assertBucket({ + index: 2550, + lpBalance: 10_000 * 1e18, + collateral: 0, + deposit: 10_001.70173768409813 * 1e18, + exchangeRate: 1.000170173768409813 * 1e18 + }); // check borrower info after fully repay - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 0, - borrowerCollateral: 0, - borrowert0Np: 0, - borrowerCollateralization: 1 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 0, + borrowerCollateral: 0, + borrowert0Np: 0, + borrowerCollateralization: 1 * 1e18 + }); assertEq(_collateral.balanceOf(_borrower), 52); assertEq(_collateral.balanceOf(address(_pool)), 0); @@ -443,86 +390,77 @@ contract ERC721SubsetPoolBorrowTest is ERC721PoolBorrowTest { function testPoolRepayRequireChecks() external tearDown { // add initial quote to the pool - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 2550 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 2551 - } - ); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2550 + }); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2551 + }); deal(address(_quote), _borrower, _quote.balanceOf(_borrower) + 10_000 * 1e18); + // should revert if borrower has no debt - _assertRepayNoDebtRevert( - { - from: _borrower, - borrower: _borrower, - amount: 10_000 * 1e18 - } - ); + _assertRepayNoDebtRevert({ + from: _borrower, + borrower: _borrower, + amount: 10_000 * 1e18 + }); // borrower 1 borrows 1000 quote from the pool uint256[] memory tokenIdsToAdd = new uint256[](3); tokenIdsToAdd[0] = 1; tokenIdsToAdd[1] = 3; tokenIdsToAdd[2] = 5; - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - tokenIds: tokenIdsToAdd - } - ); - _borrow( - { - from: _borrower, - amount: 1_000 * 1e18, - indexLimit: 3_000, - newLup: 3_010.892022197881557845 * 1e18 - } - ); + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + tokenIds: tokenIdsToAdd + }); + _borrow({ + from: _borrower, + amount: 1_000 * 1e18, + indexLimit: 3_000, + newLup: 3_010.892022197881557845 * 1e18 + }); - _assertLoans( - { - noOfLoans: 1, - maxBorrower: _borrower, - maxThresholdPrice: 333.653846153846154 * 1e18 - } - ); + _assertLoans({ + noOfLoans: 1, + maxBorrower: _borrower, + maxThresholdPrice: 333.653846153846154 * 1e18 + }); + + // should revert if LUP is below the limit + ( , , , , , uint256 lupIndex ) = _poolUtils.poolPricesInfo(address(_pool)); + _assertPullLimitIndexRevert({ + from: _borrower, + amount: 2, + indexLimit: lupIndex - 1 + }); // borrower 2 borrows 3k quote from the pool and becomes new queue HEAD tokenIdsToAdd = new uint256[](1); tokenIdsToAdd[0] = 53; - _pledgeCollateral( - { - from: _borrower2, - borrower: _borrower2, - tokenIds: tokenIdsToAdd - } - ); - _borrow( - { - from: _borrower2, - amount: 3_000 * 1e18, - indexLimit: 3_000, - newLup: 3_010.892022197881557845 * 1e18 - } - ); + _pledgeCollateral({ + from: _borrower2, + borrower: _borrower2, + tokenIds: tokenIdsToAdd + }); + _borrow({ + from: _borrower2, + amount: 3_000 * 1e18, + indexLimit: 3_000, + newLup: 3_010.892022197881557845 * 1e18 + }); - _assertLoans( - { - noOfLoans: 2, - maxBorrower: _borrower2, - maxThresholdPrice: 3_002.884615384615386 * 1e18 - } - ); + _assertLoans({ + noOfLoans: 2, + maxBorrower: _borrower2, + maxThresholdPrice: 3_002.884615384615386 * 1e18 + }); // should be able to repay loan if properly specified _repayDebt({ @@ -537,49 +475,39 @@ contract ERC721SubsetPoolBorrowTest is ERC721PoolBorrowTest { function testRepayLoanFromDifferentActor() external tearDown { - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 2550 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 2551 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 2552 - } - ); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2550 + }); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2551 + }); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2552 + }); // borrower deposits three NFTs into the subset pool uint256[] memory tokenIdsToAdd = new uint256[](3); tokenIdsToAdd[0] = 1; tokenIdsToAdd[1] = 3; tokenIdsToAdd[2] = 5; - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - tokenIds: tokenIdsToAdd - } - ); + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + tokenIds: tokenIdsToAdd + }); // borrower borrows from the pool - _borrow( - { - from: _borrower, - amount: 3_000 * 1e18, - indexLimit: 2_551, - newLup: 3_010.892022197881557845 * 1e18 - } - ); + _borrow({ + from: _borrower, + amount: 3_000 * 1e18, + indexLimit: 2_551, + newLup: 3_010.892022197881557845 * 1e18 + }); // check token balances after borrow assertEq(_pool.pledgedCollateral(), Maths.wad(3)); @@ -616,71 +544,54 @@ contract ERC721SubsetPoolBorrowTest is ERC721PoolBorrowTest { } } -contract ERC721CollectionPoolBorrowTest is ERC721PoolBorrowTest { - uint internal _anonBorrowerCount = 0; +contract ERC721CollectionPoolBorrowTest is ERC721NDecimalsHelperContract(18) { + address internal _borrower; + address internal _lender; - function createPool() external override returns (ERC721Pool) { - return _deployCollectionPool(); - } + function setUp() external { + _borrower = makeAddr("borrower"); + _lender = makeAddr("lender"); - /** - * @dev Creates debt for an anonymous non-player borrower not otherwise involved in the test. - **/ - function _anonBorrowerDrawsDebt(uint256 loanAmount) internal { - _anonBorrowerCount += 1; - address borrower = makeAddr(string(abi.encodePacked("anonBorrower", _anonBorrowerCount))); - vm.stopPrank(); - _mintAndApproveCollateralTokens(borrower, 1); - uint256[] memory tokenIdsToAdd = new uint256[](1); - tokenIdsToAdd[0] = _collateral.totalSupply(); - - _drawDebtNoLupCheck( - { - from: borrower, - borrower: borrower, - amountToBorrow: loanAmount, - limitIndex: 7_777, - tokenIds: tokenIdsToAdd - } - ); + _mintAndApproveQuoteTokens(_lender, 200_000 * 1e18); + _mintAndApproveCollateralTokens(_borrower, 52); + + vm.prank(_borrower); + _quote.approve(address(_pool), 200_000 * 1e18); } function testMinBorrowAmountCheck() external tearDown { // add initial quote to the pool changePrank(_lender); - _pool.addQuoteToken(20_000 * 1e18, 2550); + _pool.addQuoteToken(20_000 * 1e18, 2550, block.timestamp + 1 minutes); // 10 borrowers draw debt for (uint i=0; i<10; ++i) { _anonBorrowerDrawsDebt(1_200 * 1e18); } + (, uint256 loansCount, , , ) = _poolUtils.poolLoansInfo(address(_pool)); assertEq(loansCount, 10); uint256[] memory tokenIdsToAdd = new uint256[](1); tokenIdsToAdd[0] = 5; - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - tokenIds: tokenIdsToAdd - } - ); + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + tokenIds: tokenIdsToAdd + }); // should revert if borrower attempts to borrow more than minimum amount - _assertBorrowMinDebtRevert( - { - from: _borrower, - amount: 100 * 1e18, - indexLimit: 7_777 - } - ); + _assertBorrowMinDebtRevert({ + from: _borrower, + amount: 100 * 1e18, + indexLimit: MAX_FENWICK_INDEX + }); } function testMinRepayAmountCheck() external tearDown { // add initial quote to the pool changePrank(_lender); - _pool.addQuoteToken(20_000 * 1e18, 2550); + _pool.addQuoteToken(20_000 * 1e18, 2550, block.timestamp + 1 minutes); // 9 other borrowers draw debt for (uint i=0; i<9; ++i) { @@ -693,31 +604,74 @@ contract ERC721CollectionPoolBorrowTest is ERC721PoolBorrowTest { tokenIdsToAdd[1] = 3; tokenIdsToAdd[2] = 5; - _drawDebtNoLupCheck( - { - from: _borrower, - borrower: _borrower, - amountToBorrow: 1_000 * 1e18, - limitIndex: 3000, - tokenIds: tokenIdsToAdd - } - ); + _drawDebtNoLupCheck({ + from: _borrower, + borrower: _borrower, + amountToBorrow: 1_000 * 1e18, + limitIndex: 3000, + tokenIds: tokenIdsToAdd + }); (, uint256 loansCount, , , ) = _poolUtils.poolLoansInfo(address(_pool)); assertEq(loansCount, 10); // should revert if amount left after repay is less than the average debt - _assertRepayMinDebtRevert( - { - from: _borrower, - borrower: _borrower, - amount: 900 * 1e18 - } - ); + _assertRepayMinDebtRevert({ + from: _borrower, + borrower: _borrower, + amount: 900 * 1e18 + }); + } +} + +contract ERC721ScaledQuoteTokenBorrowTest is ERC721NDecimalsHelperContract(4) { + address internal _borrower; + address internal _lender; + + function setUp() external { + _borrower = makeAddr("borrower"); + _lender = makeAddr("lender"); + + _mintAndApproveQuoteTokens(_lender, 20_000 * 1e4); + _mintAndApproveCollateralTokens(_borrower, 5); } + function testMinDebtBelowDustLimitCheck() external tearDown { + // add initial quote to the pool + changePrank(_lender); + _pool.addQuoteToken(20_000 * 1e18, 2550, block.timestamp + 30); + + // borrower pledges a single NFT + uint256[] memory tokenIdsToAdd = new uint256[](1); + tokenIdsToAdd[0] = 5; + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + tokenIds: tokenIdsToAdd + }); + + // should revert if borrower tries to draw debt below dust limit + _assertBorrowDustRevert({ + from: _borrower, + amount: 0.00005 * 1e18, + indexLimit: 2550 + }); + + // 10 borrowers draw debt at the dust limit + for (uint i=0; i<10; ++i) { + _anonBorrowerDrawsDebt(0.0001 * 1e18); + } + + // should still revert if borrower tries to draw debt below dust limit + _assertBorrowDustRevert({ + from: _borrower, + amount: 0.000075 * 1e18, + indexLimit: 2550 + }); + } } + contract ERC721PoolBorrowFuzzyTest is ERC721FuzzyHelperContract { address internal _borrower; @@ -754,22 +708,23 @@ contract ERC721PoolBorrowFuzzyTest is ERC721FuzzyHelperContract { uint256[] memory indexes = new uint256[](numIndexes); for (uint256 i = 0; i < numIndexes; ++i) { deal(address(_quote), _lender, mintAmount_); + indexes[i] = _randomIndexWithMinimumPrice(5000); // setting a minimum price for collateral prevents exceeding memory and gas limits _addLiquidity({ from: _lender, amount: mintAmount_, index: indexes[i], - lpAward: mintAmount_ * 1e9, + lpAward: mintAmount_, newLup: _calculateLup(address(_pool), 0) }); _assertBucket({ index: indexes[i], - lpBalance: mintAmount_ * 1e9, + lpBalance: mintAmount_, collateral: 0, deposit: mintAmount_, - exchangeRate: 1e27 + exchangeRate: 1e18 }); } @@ -792,10 +747,10 @@ contract ERC721PoolBorrowFuzzyTest is ERC721FuzzyHelperContract { for (uint256 i = 0; i < numIndexes; ++i) { _assertBucket({ index: indexes[i], - lpBalance: mintAmount_ * 1e9, + lpBalance: mintAmount_, collateral: 0, deposit: mintAmount_, - exchangeRate: 1e27 + exchangeRate: 1e18 }); } @@ -832,6 +787,7 @@ contract ERC721PoolBorrowFuzzyTest is ERC721FuzzyHelperContract { // repay all debt and withdraw collateral (debt, , ) = _poolUtils.borrowerInfo(address(_pool), address(_borrower)); deal(address(_quote), _borrower, debt); + _repayDebt({ from: _borrower, borrower: _borrower, @@ -848,16 +804,17 @@ contract ERC721PoolBorrowFuzzyTest is ERC721FuzzyHelperContract { // check that only deposits above the htp earned interest if (indexes[i] <= _poolUtils.priceToIndex(Maths.wdiv(debt, Maths.wad(tokenIdsToAdd.length)))) { assertGt(deposit, mintAmount_); - assertGt(exchangeRate, 1e27); + assertGt(exchangeRate, 1e18); } else { assertEq(deposit, mintAmount_); - assertEq(exchangeRate, 1e27); + assertEq(exchangeRate, 1e18); } - assertEq(lpAccumulator, mintAmount_ * 1e9); + assertEq(lpAccumulator, mintAmount_); + _assertBucket({ index: indexes[i], - lpBalance: mintAmount_ * 1e9, + lpBalance: mintAmount_, collateral: 0, deposit: deposit, exchangeRate: exchangeRate diff --git a/tests/forge/ERC721Pool/ERC721PoolCollateral.t.sol b/tests/forge/ERC721Pool/ERC721PoolCollateral.t.sol index cee499c25..89c99740a 100644 --- a/tests/forge/ERC721Pool/ERC721PoolCollateral.t.sol +++ b/tests/forge/ERC721Pool/ERC721PoolCollateral.t.sol @@ -3,6 +3,8 @@ pragma solidity 0.8.14; import { ERC721HelperContract } from './ERC721DSTestPlus.sol'; +import { ERC721Pool } from 'src/ERC721Pool.sol'; + import 'src/PoolInfoUtils.sol'; import 'src/libraries/helpers/PoolHelper.sol'; @@ -13,20 +15,14 @@ contract ERC721PoolCollateralTest is ERC721HelperContract { address internal _lender; address internal _lender2; - function setUp() external { + function setUp() virtual external { _borrower = makeAddr("borrower"); _borrower2 = makeAddr("borrower2"); _lender = makeAddr("lender"); _lender2 = makeAddr("lender2"); - // deploy subset pool - uint256[] memory subsetTokenIds = new uint256[](5); - subsetTokenIds[0] = 1; - subsetTokenIds[1] = 3; - subsetTokenIds[2] = 5; - subsetTokenIds[3] = 51; - subsetTokenIds[4] = 53; - _pool = _deploySubsetPool(subsetTokenIds); + // deploy collection pool + _pool = _deployCollectionPool(); _mintAndApproveQuoteTokens(_lender, 200_000 * 1e18); _mintAndApproveQuoteTokens(_borrower, 100 * 1e18); @@ -35,15 +31,11 @@ contract ERC721PoolCollateralTest is ERC721HelperContract { _mintAndApproveCollateralTokens(_borrower2, 53); } - /*******************************/ - /*** ERC721 Collection Tests ***/ - /*******************************/ + /************************************/ + /*** ERC721 Collection Pool Tests ***/ + /************************************/ - /***************************/ - /*** ERC721 Subset Tests ***/ - /***************************/ - - function testPledgeCollateralSubset() external tearDown { + function testPledgeCollateral() external tearDown { // check initial token balances assertEq(_pool.pledgedCollateral(), 0); @@ -55,14 +47,12 @@ contract ERC721PoolCollateralTest is ERC721HelperContract { tokenIdsToAdd[1] = 3; tokenIdsToAdd[2] = 5; - // borrower deposits three NFTs into the subset pool - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - tokenIds: tokenIdsToAdd - } - ); + // borrower deposits three NFTs into the pool + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + tokenIds: tokenIdsToAdd + }); // check token balances after add assertEq(_pool.pledgedCollateral(), Maths.wad(3)); @@ -70,22 +60,7 @@ contract ERC721PoolCollateralTest is ERC721HelperContract { assertEq(_collateral.balanceOf(address(_pool)), 3); } - function testPledgeCollateralNotInSubset() external tearDown { - uint256[] memory tokenIdsToAdd = new uint256[](3); - tokenIdsToAdd[0] = 2; - tokenIdsToAdd[1] = 4; - tokenIdsToAdd[2] = 6; - - // should revert if borrower attempts to add tokens not in the pool subset - _assertPledgeCollateralNotInSubsetRevert( - { - from: _borrower, - tokenIds: tokenIdsToAdd - } - ); - } - - function testPledgeCollateralInSubsetFromDifferentActor() external tearDown { + function testPledgeCollateralFromDifferentActor() external tearDown { // check initial token balances assertEq(_pool.pledgedCollateral(), 0); @@ -93,36 +68,30 @@ contract ERC721PoolCollateralTest is ERC721HelperContract { assertEq(_collateral.balanceOf(_borrower2), 53); assertEq(_collateral.balanceOf(address(_pool)), 0); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 0, - borrowerCollateral: 0, - borrowert0Np: 0, - borrowerCollateralization: 1 * 1e18 - } - ); - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 0, - borrowerCollateral: 0, - borrowert0Np: 0, - borrowerCollateralization: 1 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 0, + borrowerCollateral: 0, + borrowert0Np: 0, + borrowerCollateralization: 1 * 1e18 + }); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 0, + borrowerCollateral: 0, + borrowert0Np: 0, + borrowerCollateralization: 1 * 1e18 + }); uint256[] memory tokenIdsToAdd = new uint256[](1); tokenIdsToAdd[0] = 53; - // borrower deposits three NFTs into the subset pool - _pledgeCollateral( - { - from: _borrower2, - borrower: _borrower, - tokenIds: tokenIdsToAdd - } - ); + // borrower deposits three NFTs into the pool + _pledgeCollateral({ + from: _borrower2, + borrower: _borrower, + tokenIds: tokenIdsToAdd + }); // check token balances after add assertEq(_pool.pledgedCollateral(), Maths.wad(1)); @@ -131,24 +100,20 @@ contract ERC721PoolCollateralTest is ERC721HelperContract { assertEq(_collateral.balanceOf(_borrower2), 52); assertEq(_collateral.balanceOf(address(_pool)), 1); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 0, - borrowerCollateral: 1 * 1e18, - borrowert0Np: 0, - borrowerCollateralization: 1 * 1e18 - } - ); - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 0, - borrowerCollateral: 0, - borrowert0Np: 0, - borrowerCollateralization: 1 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 0, + borrowerCollateral: 1 * 1e18, + borrowert0Np: 0, + borrowerCollateralization: 1 * 1e18 + }); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 0, + borrowerCollateral: 0, + borrowert0Np: 0, + borrowerCollateralization: 1 * 1e18 + }); } function testPullCollateral() external tearDown { @@ -169,14 +134,12 @@ contract ERC721PoolCollateralTest is ERC721HelperContract { tokenIdsToAdd[1] = 3; tokenIdsToAdd[2] = 5; - // borrower deposits three NFTs into the subset pool - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - tokenIds: tokenIdsToAdd - } - ); + // borrower deposits three NFTs into the pool + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + tokenIds: tokenIdsToAdd + }); // check token balances after add assertEq(_pool.pledgedCollateral(), Maths.wad(3)); @@ -191,25 +154,21 @@ contract ERC721PoolCollateralTest is ERC721HelperContract { assertEq(_collateral.ownerOf(5), address(_pool)); // should fail if trying to pull collateral by an address without pledged collateral - _assertPullInsufficientCollateralRevert( - { - from: _lender, - amount: 3 - } - ); + _assertPullInsufficientCollateralRevert({ + from: _lender, + amount: 3 + }); // borrower2 is owner of NFT assertEq(_collateral.ownerOf(53), _borrower2); tokenIdsToAdd = new uint256[](1); tokenIdsToAdd[0] = 53; - _pledgeCollateral( - { - from: _borrower2, - borrower: _borrower2, - tokenIds: tokenIdsToAdd - } - ); + _pledgeCollateral({ + from: _borrower2, + borrower: _borrower2, + tokenIds: tokenIdsToAdd + }); // check token balances after add assertEq(_pool.pledgedCollateral(), Maths.wad(4)); @@ -242,14 +201,96 @@ contract ERC721PoolCollateralTest is ERC721HelperContract { assertEq(_collateral.ownerOf(3), _borrower); assertEq(_collateral.ownerOf(5), _borrower); - // should fail if borrower tries to pull more NFTs than remaining in pool - _assertPullInsufficientCollateralRevert( - { - from: _borrower, - amount: 3 - } - ); + _assertPullInsufficientCollateralRevert({ + from: _borrower, + amount: 3 + }); + } + + function testPullCollateralToDifferentRecipient() external tearDown { + address tokensReceiver = makeAddr("tokensReceiver"); + + // check initial token balances + assertEq(_pool.pledgedCollateral(), 0); + + assertEq(_collateral.balanceOf(_borrower), 52); + assertEq(_collateral.balanceOf(_borrower2), 53); + assertEq(_collateral.balanceOf(tokensReceiver), 0); + assertEq(_collateral.balanceOf(address(_pool)), 0); + + // borrower is owner of NFTs + assertEq(_collateral.ownerOf(1), _borrower); + assertEq(_collateral.ownerOf(3), _borrower); + assertEq(_collateral.ownerOf(5), _borrower); + + uint256[] memory tokenIdsToAdd = new uint256[](3); + tokenIdsToAdd[0] = 1; + tokenIdsToAdd[1] = 3; + tokenIdsToAdd[2] = 5; + + // borrower deposits three NFTs into the pool + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + tokenIds: tokenIdsToAdd + }); + + // borrower2 deposits three NFTs into the pool + tokenIdsToAdd = new uint256[](1); + tokenIdsToAdd[0] = 53; + _pledgeCollateral({ + from: _borrower2, + borrower: _borrower2, + tokenIds: tokenIdsToAdd + }); + + // check token balances after add + assertEq(_pool.pledgedCollateral(), Maths.wad(4)); + assertEq(_collateral.balanceOf(address(_pool)), 4); + + // pool is owner of pledged NFTs + assertEq(_collateral.ownerOf(1), address(_pool)); + assertEq(_collateral.ownerOf(3), address(_pool)); + assertEq(_collateral.ownerOf(5), address(_pool)); + assertEq(_collateral.ownerOf(53), address(_pool)); + + // borrower removes some of their deposited NFTs from the pool and transfer to a different recipient + changePrank(_borrower); + ERC721Pool(address(_pool)).repayDebt(_borrower, 0, 2, tokensReceiver, MAX_FENWICK_INDEX); + + // check token balances after remove + assertEq(_pool.pledgedCollateral(), Maths.wad(2)); + + assertEq(_collateral.balanceOf(_borrower), 49); + assertEq(_collateral.balanceOf(_borrower2), 52); + assertEq(_collateral.balanceOf(tokensReceiver), 2); + assertEq(_collateral.balanceOf(address(_pool)), 2); + + // pool is owner of remaining pledged NFT + assertEq(_collateral.ownerOf(1), address(_pool)); + // recipient is owner of 2 pulled NFTs + assertEq(_collateral.ownerOf(3), tokensReceiver); + assertEq(_collateral.ownerOf(5), tokensReceiver); + + // borrower2 removes deposited NFT from the pool and transfer to same recipient + changePrank(_borrower2); + ERC721Pool(address(_pool)).repayDebt(_borrower2, 0, 1, tokensReceiver, MAX_FENWICK_INDEX); + + // check token balances after remove + assertEq(_pool.pledgedCollateral(), Maths.wad(1)); + + assertEq(_collateral.balanceOf(_borrower), 49); + assertEq(_collateral.balanceOf(_borrower2), 52); + assertEq(_collateral.balanceOf(tokensReceiver), 3); + assertEq(_collateral.balanceOf(address(_pool)), 1); + + // pool is owner of remaining pledged NFT + assertEq(_collateral.ownerOf(1), address(_pool)); + // recipient is owner of 3 pulled NFTs + assertEq(_collateral.ownerOf(3), tokensReceiver); + assertEq(_collateral.ownerOf(5), tokensReceiver); + assertEq(_collateral.ownerOf(53), tokensReceiver); } function testPullCollateralNotInPool() external tearDown { @@ -263,13 +304,11 @@ contract ERC721PoolCollateralTest is ERC721HelperContract { tokenIdsToAdd[1] = 3; tokenIdsToAdd[2] = 5; - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - tokenIds: tokenIdsToAdd - } - ); + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + tokenIds: tokenIdsToAdd + }); // pool is owner of pledged NFTs assertEq(_collateral.ownerOf(1), address(_pool)); @@ -277,12 +316,10 @@ contract ERC721PoolCollateralTest is ERC721HelperContract { assertEq(_collateral.ownerOf(5), address(_pool)); // should revert if borrower attempts to remove more collateral than pledged from pool - _assertPullInsufficientCollateralRevert( - { - from: _borrower, - amount: 5 - } - ); + _assertPullInsufficientCollateralRevert({ + from: _borrower, + amount: 5 + }); // borrower should be able to remove collateral in the pool _repayDebtNoLupCheck({ @@ -299,27 +336,21 @@ contract ERC721PoolCollateralTest is ERC721HelperContract { } function testPullCollateralPartiallyEncumbered() external tearDown { - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 2552 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 2551 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 2550 - } - ); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2552 + }); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2551 + }); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2550 + }); // check initial token balances assertEq(_collateral.balanceOf(_borrower), 52); @@ -352,22 +383,18 @@ contract ERC721PoolCollateralTest is ERC721HelperContract { tokenIdsToAdd[1] = 3; tokenIdsToAdd[2] = 5; - // borrower deposits three NFTs into the subset pool - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - tokenIds: tokenIdsToAdd - } - ); - _borrow( - { - from: _borrower, - amount: 3_000 * 1e18, - indexLimit: 2_551, - newLup: _priceAt(2550) - } - ); + // borrower deposits three NFTs into the pool + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + tokenIds: tokenIdsToAdd + }); + _borrow({ + from: _borrower, + amount: 3_000 * 1e18, + indexLimit: 2_551, + newLup: _priceAt(2550) + }); // check token balances after borrow assertEq(_collateral.balanceOf(_borrower), 49); @@ -434,258 +461,212 @@ contract ERC721PoolCollateralTest is ERC721HelperContract { function testPullCollateralOverlyEncumbered() external tearDown { // lender deposits 10000 Quote into 3 buckets - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 2552 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 2551 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 2550 - } - ); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2552 + }); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2551 + }); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2550 + }); uint256[] memory tokenIdsToAdd = new uint256[](3); tokenIdsToAdd[0] = 1; tokenIdsToAdd[1] = 3; tokenIdsToAdd[2] = 5; - // borrower deposits three NFTs into the subset pool - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - tokenIds: tokenIdsToAdd - } - ); + // borrower deposits three NFTs into the pool + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + tokenIds: tokenIdsToAdd + }); // check collateralization after pledge (uint256 poolDebt,,) = _pool.debtInfo(); assertEq(_encumberance(poolDebt, _lup()), 0); // borrower borrows some quote - _borrow( - { - from: _borrower, - amount: 9_000 * 1e18, - indexLimit: 2_551, - newLup: _priceAt(2550) - } - ); + _borrow({ + from: _borrower, + amount: 9_000 * 1e18, + indexLimit: 2_551, + newLup: _priceAt(2550) + }); // check collateralization after borrow (poolDebt,,) = _pool.debtInfo(); assertEq(_encumberance(poolDebt, _lup()), 2.992021560300836411 * 1e18); // should revert if borrower attempts to pull more collateral than is unencumbered - _assertPullInsufficientCollateralRevert( - { - from: _borrower, - amount: 2 - } - ); + _assertPullInsufficientCollateralRevert({ + from: _borrower, + amount: 2 + }); } function testAddRemoveCollateral() external tearDown { // lender adds some liquidity - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 1692 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 1530 - } - ); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 1692 + }); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 1530 + }); uint256[] memory tokenIds = new uint256[](2); tokenIds[0] = 1; tokenIds[1] = 5; // add three tokens to a single bucket - _addCollateral( - { - from: _borrower, - tokenIds: tokenIds, - index: 1530, - lpAward: 975_232.505322350083963682 * 1e27 - } - ); + _addCollateral({ + from: _borrower, + tokenIds: tokenIds, + index: 1530, + lpAward: 975_232.505322350083963682 * 1e18 + }); // should revert if the actor does not have any LP to remove a token - _assertRemoveCollateralInsufficientLPsRevert( - { - from: _borrower2, - amount: 1, - index: 1530 - } - ); + _assertRemoveCollateralInsufficientLPsRevert({ + from: _borrower2, + amount: 1, + index: 1530 + }); // should revert if we try to remove a token from a bucket with no collateral - _assertRemoveInsufficientCollateralRevert( - { - from: _borrower, - amount: 1, - index: 1692 - } - ); + _assertRemoveInsufficientCollateralRevert({ + from: _borrower, + amount: 1, + index: 1692 + }); // remove one token - _removeCollateral( - { - from: _borrower, - amount: 1, - index: 1530, - lpRedeem: 487_616.252661175041981841 * 1e27 - } - ); + _removeCollateral({ + from: _borrower, + amount: 1, + index: 1530, + lpRedeem: 487_616.252661175041981841 * 1e18 + }); + + _assertBucket({ + index: 1530, + lpBalance: 497_616.252661175041981841 * 1e18, + collateral: Maths.wad(1), + deposit: 10_000 * 1e18, + exchangeRate: 1 * 1e18 + }); + _assertLenderLpBalance({ + lender: _borrower, + index: 1530, + lpBalance: 487_616.252661175041981841 * 1e18, + depositTime: _startTime + }); - _assertBucket( - { - index: 1530, - lpBalance: 497_616.252661175041981841 * 1e27, - collateral: Maths.wad(1), - deposit: 10_000 * 1e18, - exchangeRate: 1 * 1e27 - } - ); - _assertLenderLpBalance( - { - lender: _borrower, - index: 1530, - lpBalance: 487_616.252661175041981841 * 1e27, - depositTime: _startTime - } - ); // remove another token - _removeCollateral( - { - from: _borrower, - amount: 1, - index: 1530, - lpRedeem: 487_616.252661175041981841 * 1e27 - } - ); + _removeCollateral({ + from: _borrower, + amount: 1, + index: 1530, + lpRedeem: 487_616.252661175041981841 * 1e18 + }); - _assertBucket( - { - index: 1530, - lpBalance: 10_000 * 1e27, - collateral: 0, - deposit: 10_000 * 1e18, - exchangeRate: 1 * 1e27 - } - ); - _assertLenderLpBalance( - { - lender: _borrower, - index: 1530, - lpBalance: 0, - depositTime: _startTime - } - ); + _assertBucket({ + index: 1530, + lpBalance: 10_000 * 1e18, + collateral: 0, + deposit: 10_000 * 1e18, + exchangeRate: 1 * 1e18 + }); + _assertLenderLpBalance({ + lender: _borrower, + index: 1530, + lpBalance: 0, + depositTime: _startTime + }); // lender removes quote token skip(1 days); // skip to avoid penalty - _removeAllLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 1530, - newLup: MAX_PRICE, - lpRedeem: 10_000 * 1e27 - } - ); - _assertBucket( - { - index: 1530, - lpBalance: 0, - collateral: 0, - deposit: 0, - exchangeRate: 1 * 1e27 - } - ); + _removeAllLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 1530, + newLup: MAX_PRICE, + lpRedeem: 10_000 * 1e18 + }); + + _assertBucket({ + index: 1530, + lpBalance: 0, + collateral: 0, + deposit: 0, + exchangeRate: 1 * 1e18 + }); } function testMergeOrRemoveCollateral() external tearDown { for (uint256 i = 3060; i < (3060 + 10); i++) { - _addLiquidity( - { - from: _lender, - amount: 20 * 1e18, - index: i, - newLup: MAX_PRICE, - lpAward: 20 * 1e27 - } - ); + _addLiquidity({ + from: _lender, + amount: 20 * 1e18, + index: i, + newLup: MAX_PRICE, + lpAward: 20 * 1e18 + }); } // borrower pledge collateral and draws debt uint256[] memory tokenIdsToAdd = new uint256[](2); tokenIdsToAdd[0] = 1; tokenIdsToAdd[1] = 3; - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - tokenIds: tokenIdsToAdd - } - ); - _borrow( - { - from: _borrower, - amount: 150 * 1e18, - indexLimit: 8191, - newLup: 228.476350374240318479 * 1e18 - } - ); + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + tokenIds: tokenIdsToAdd + }); + _borrow({ + from: _borrower, + amount: 150 * 1e18, + indexLimit: MAX_FENWICK_INDEX, + newLup: 228.476350374240318479 * 1e18 + }); // Borrower starts with possession of tokens 1 and 3 uint256[] memory borrowerTokenIds = new uint256[](2); borrowerTokenIds[0] = 1; borrowerTokenIds[1] = 3; - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 150.144230769230769300 * 1e18, - borrowerCollateral: 2.0 * 1e18, - borrowert0Np: 78.825721153846153882 * 1e18, - borrowerCollateralization: 3.043424968161510485 * 1e18, - tokenIds: borrowerTokenIds - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 150.144230769230769300 * 1e18, + borrowerCollateral: 2.0 * 1e18, + borrowert0Np: 78.825721153846153882 * 1e18, + borrowerCollateralization: 3.043424968161510485 * 1e18, + tokenIds: borrowerTokenIds + }); // skip to render borrower undercollateralized skip(10000 days); - _kick( - { - from: _lender, - borrower: _borrower, - debt: 598.174133241016922933 * 1e18, - collateral: 2.0 * 1e18, - bond: 5.907892673985352325 * 1e18, - transferAmount: 5.907892673985352325 * 1e18 - } - ); + _kick({ + from: _lender, + borrower: _borrower, + debt: 598.174133241016922932 * 1e18, + collateral: 2.0 * 1e18, + bond: 5.907892673985352325 * 1e18, + transferAmount: 5.907892673985352325 * 1e18 + }); skip(32 hours); @@ -724,50 +705,58 @@ contract ERC721PoolCollateralTest is ERC721HelperContract { }) ); + // force an interest accumulation to assert bucket with interest + _addLiquidity({ + from: _lender, + amount: 0 * 1e18, + index: 7000, + newLup: 99836282890, + lpAward: 0 + }); + _assertBucket({ + index: 3060, + lpBalance: 20 * 1e18, + collateral: 0.0000000000000000000 * 1e18, + deposit: 20.010216420146293860 * 1e18, + exchangeRate: 1.000510821007314693 * 1e18 + }); + // Before depositTake: NFTs pledged by liquidated borrower are owned by the borrower in the pool assertEq(_collateral.ownerOf(1), address(_pool)); assertEq(_collateral.ownerOf(3), address(_pool)); // exchange collateral for lpb 3060 - 3070, going down in price for (uint256 i = _i236_59; i < (3060 + 10); i++) { - _depositTake( - { - from: _lender, - borrower: _borrower, - index: i - } - ); + _depositTake({ + from: _lender, + borrower: _borrower, + index: i + }); } - _assertBucket( - { - index: 3060, - lpBalance: 20.2 * 1e27, - collateral: 0.085430491711717314 * 1e18, - deposit: 0, - exchangeRate: 1.000610882095524250072170475 * 1e27 - } - ); + _assertBucket({ + index: 3060, + lpBalance: 20.202020202020202022 * 1e18, + collateral: 0.085430491711717314 * 1e18, + deposit: 0, + exchangeRate: 1.000510821007314698 * 1e18 + }); - _assertBucket( - { - index: 3061, - lpBalance: 20.2 * 1e27, - collateral: 0.085857644170275899 * 1e18, - deposit: 0, - exchangeRate: 1.000610882095524239992886155 * 1e27 - } - ); + _assertBucket({ + index: 3061, + lpBalance: 20.202020202020202019 * 1e18, + collateral: 0.085857644170275899 * 1e18, + deposit: 0, + exchangeRate: 1.000510821007314688 * 1e18 + }); - _assertBucket( - { - index: 3070, - lpBalance: 0, - collateral: 0, - deposit: 0, - exchangeRate: 1 * 1e27 - } - ); + _assertBucket({ + index: 3070, + lpBalance: 0, + collateral: 0, + deposit: 0, + exchangeRate: 1 * 1e18 + }); _assertAuction( AuctionParams({ @@ -795,7 +784,7 @@ contract ERC721PoolCollateralTest is ERC721HelperContract { encumberedCollateral: 4407944209.541175956055268556 * 1e18, poolDebt: 440.072765067090279852 * 1e18, actualUtilization: 0, - targetUtilization: 3_123_578_486.651416548727612650 * 1e18, + targetUtilization: 2_996_091_127.870826153174895975 * 1e18, minDebtAmount: 0, loans: 0, maxBorrower: address(0), @@ -808,51 +797,52 @@ contract ERC721PoolCollateralTest is ERC721HelperContract { borrowerTokenIds = new uint256[](1); borrowerTokenIds[0] = 1; - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 440.072765067090279852 * 1e18, - borrowerCollateral: 1.126214674710621229 * 1e18, - borrowert0Np: 78.825721153846153882 * 1e18, - borrowerCollateralization: 0.000000000255496581 * 1e18, - tokenIds: borrowerTokenIds - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 440.072765067090279852 * 1e18, + borrowerCollateral: 1.126214674710621229 * 1e18, + borrowert0Np: 78.825721153846153882 * 1e18, + borrowerCollateralization: 0.000000000255496581 * 1e18, + tokenIds: borrowerTokenIds + }); // after depositTake but before take: NFTs pledged by liquidated borrower are owned by the pool assertEq(_collateral.ownerOf(1), address(_pool)); assertEq(_collateral.ownerOf(3), address(_pool)); - _take( - { - from: _lender, - borrower: _borrower, - maxCollateral: 2.0 * 1e18, - bondChange: 0.000000052051493471 * 1e18, - givenAmount: 0.000005205149347131 * 1e18, - collateralTaken: 1.126214674710621229 * 1e18, - isReward: true - } - ); + _take({ + from: _lender, + borrower: _borrower, + maxCollateral: 2 * 1e18, + bondChange: 0.000000046218092021 * 1e18, + givenAmount: 0.000004621809202112 * 1e18, + collateralTaken: 1 * 1e18, + isReward: true + }); // Borrower has < 1 collateral, no NFTs in possession borrowerTokenIds = new uint256[](0); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 440.072759913992426192 * 1e18, - borrowerCollateral: 0.126214674710621229 * 1e18, - borrowert0Np: 78.825721153846153882 * 1e18, - borrowerCollateralization: 0.000000000028633456 * 1e18, - tokenIds: borrowerTokenIds - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 440.072760491499169762 * 1e18, + borrowerCollateral: 0.126214674710621229 * 1e18, + borrowert0Np: 78.825721153846153882 * 1e18, + borrowerCollateralization: 0.000000000028633456 * 1e18, + tokenIds: borrowerTokenIds + }); // after take: NFT with ID 1, pledged by liquidated borrower is owned by the taker assertEq(_collateral.ownerOf(1), address(_pool)); assertEq(_collateral.ownerOf(3), _lender); + // subsequent take should fail as there's less than 1 NFT remaining in the loan + _assertTakeInsufficentCollateralRevert({ + from: _lender, + borrower: _borrower2, + maxCollateral: 1 * 1e18 + }); + // 70.16 hours skip(4210 minutes); @@ -861,37 +851,33 @@ contract ERC721PoolCollateralTest is ERC721HelperContract { borrower: _borrower, active: true, kicker: address(_lender), - bondSize: 5.907892726036845796 * 1e18, + bondSize: 5.907892720203444346 * 1e18, bondFactor: 0.010 * 1e18, kickTime: block.timestamp - (32 hours + 4210 minutes), kickMomp: 0.000000099836282890 * 1e18, - totalBondEscrowed: 5.907892726036845796 * 1e18, + totalBondEscrowed: 5.907892720203444346 * 1e18, auctionPrice: 0 * 1e18, - debtInAuction: 440.072759913992426192 * 1e18, - thresholdPrice: 3_488.390484128255500242 * 1e18, + debtInAuction: 440.072760491499169762 * 1e18, + thresholdPrice: 3_488.390488706064472517 * 1e18, neutralPrice: 310.164365384230997074 * 1e18 }) ); - _settle( - { - from: _lender, - borrower: _borrower, - maxDepth: 10, - settledDebt: 111.818402566884385900 * 1e18 - } - ); + _settle({ + from: _lender, + borrower: _borrower, + maxDepth: 10, + settledDebt: 111.818402713623487748 * 1e18 + }); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 0, - borrowerCollateral: 0, - borrowert0Np: 78.825721153846153882 * 1e18, - borrowerCollateralization: 1.0 * 1e18, - tokenIds: borrowerTokenIds - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 0, + borrowerCollateral: 0, + borrowert0Np: 78.825721153846153882 * 1e18, + borrowerCollateralization: 1.0 * 1e18, + tokenIds: borrowerTokenIds + }); // after take: NFT, 1 pledged by liquidated borrower is owned by the taker assertEq(_collateral.ownerOf(1), address(_pool)); @@ -914,49 +900,40 @@ contract ERC721PoolCollateralTest is ERC721HelperContract { }) ); - _assertBucket( - { - index: 3060, - lpBalance: 20.2 * 1e27, - collateral: 0.085430491711717314 * 1e18, - deposit: 0, - exchangeRate: 1.000610882095524250072170475 * 1e27 - } - ); - _assertBucket( - { - index: 3069, - lpBalance: 20.2 * 1e27, - collateral: 0.089352655062849951 * 1e18, - deposit: 0, - exchangeRate: 1.000610882095524241676916623 * 1e27 - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: 3069, - lpBalance: 20.2 * 1e27, - depositTime: _startTime + 10000 days + 32 hours - } - ); - _assertBucket( - { - index: 7388, - lpBalance: 0.000000012600803969278909906 * 1e27, // LPs awarded to borrower for settled collateral - collateral: 0.126214674710621229 * 1e18, // settled collateral amount - deposit: 0, - exchangeRate: 1.000000000000000000006624324 * 1e27 - } - ); - _assertLenderLpBalance( - { - lender: _borrower, - index: 7388, - lpBalance: 0.000000012600803969278909906 * 1e27, - depositTime: _startTime + 10000 days + 32 hours + 4210 minutes - } - ); + _assertBucket({ + index: 3060, + lpBalance: 20.202020202020202022 * 1e18, + collateral: 0.085430491711717314 * 1e18, + deposit: 0, + exchangeRate: 1.000510821007314698 * 1e18 + }); + _assertBucket({ + index: 3069, + lpBalance: 20.202020202020202020 * 1e18, + collateral: 0.089352655062849951 * 1e18, + deposit: 0, + exchangeRate: 1.000510821007314689 * 1e18 + }); + _assertLenderLpBalance({ + lender: _lender, + index: 3069, + lpBalance: 20.202020202020202020 * 1e18, + depositTime: _startTime + 10000 days + 32 hours + }); + _assertBucket({ + index: 7388, + lpBalance: 0.000000012600803969 * 1e18, // LPs awarded to borrower for settled collateral + collateral: 0.126214674710621229 * 1e18, // settled collateral amount + deposit: 0, + exchangeRate: 1 * 1e18 + }); + _assertLenderLpBalance({ + lender: _borrower, + index: 7388, + lpBalance: 0.000000012600803969 * 1e18, + depositTime: _startTime + 10000 days + 32 hours + 4210 minutes + }); + assertEq(_collateral.balanceOf(_lender), 1); assertEq(_collateral.balanceOf(_borrower), 50); assertEq(_collateral.balanceOf(address(_pool)), 1); @@ -968,183 +945,149 @@ contract ERC721PoolCollateralTest is ERC721HelperContract { removalIndexes[removalI] = i; removalI++; } + // Reverts because 3059 is a higher price than 3060, must merge down in price - _assertCannotMergeToHigherPriceRevert( - { - from: _lender, - toIndex: 3059, - noOfNFTsToRemove: 1.0, - removeCollateralAtIndex: removalIndexes - } - ); + _assertCannotMergeToHigherPriceRevert({ + from: _lender, + toIndex: 3059, + noOfNFTsToRemove: 1.0, + removeCollateralAtIndex: removalIndexes + }); - _mergeOrRemoveCollateral( - { - from: _lender, - toIndex: 3069, - noOfNFTsToRemove: 1.0, - collateralMerged: 0.873785325289378771 * 1e18, - removeCollateralAtIndex: removalIndexes, - toIndexLps: 197.657763058028917103677822434 * 1e27 - } - ); + _mergeOrRemoveCollateral({ + from: _lender, + toIndex: 3070, + noOfNFTsToRemove: 1.0, + collateralMerged: 0.873785325289378771 * 1e18, + removeCollateralAtIndex: removalIndexes, + toIndexLps: 196.674391102516337019 * 1e18 + }); - _assertBucket( - { - index: 3060, - lpBalance: 0, - collateral: 0, - deposit: 0, - exchangeRate: 1.0 * 1e27 - } - ); - _assertBucket( - { - index: 3061, - lpBalance: 0, - collateral: 0, - deposit: 0, - exchangeRate: 1.0 * 1e27 - } - ); - _assertBucket( - { - index: 3069, - lpBalance: 197.657763058028917103677822434 * 1e27, // new LPs amount accounting collateral merged in bucket - collateral: 0.873785325289378771 * 1e18, // reflects collateral merged in the bucket - deposit: 0, - exchangeRate: 0.999999999999999999999999999 * 1e27 - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: 3069, - lpBalance: 197.657763058028917103677822434 * 1e27, - depositTime: _startTime + 10000 days + 32 hours + 4210 minutes - } - ); - _assertBucket( - { - index: 7388, - lpBalance: 0.000000012600803969278909906 * 1e27, // LPs awarded to borrower for settled collateral - collateral: 0.126214674710621229 * 1e18, // settled collateral amount - deposit: 0, - exchangeRate: 1.000000000000000000006624324 * 1e27 - } - ); + _assertBucket({ + index: 3060, + lpBalance: 0, + collateral: 0, + deposit: 0, + exchangeRate: 1 * 1e18 + }); + _assertBucket({ + index: 3061, + lpBalance: 0, + collateral: 0, + deposit: 0, + exchangeRate: 1 * 1e18 + }); + _assertBucket({ + index: 3070, + lpBalance: 196.674391102516337019 * 1e18, // new LPs amount accounting collateral merged in bucket + collateral: 0.873785325289378771 * 1e18, // reflects collateral merged in the bucket + deposit: 0, + exchangeRate: 1 * 1e18 + }); + _assertLenderLpBalance({ + lender: _lender, + index: 3070, + lpBalance: 196.674391102516337019 * 1e18, + depositTime: _startTime + 10000 days + 32 hours + 4210 minutes + }); + _assertBucket({ + index: 7388, + lpBalance: 0.000000012600803969 * 1e18, // LPs awarded to borrower for settled collateral + collateral: 0.126214674710621229 * 1e18, // settled collateral amount + deposit: 0, + exchangeRate: 1 * 1e18 + }); assertEq(_collateral.balanceOf(_lender), 1); assertEq(_collateral.balanceOf(_borrower), 50); assertEq(_collateral.balanceOf(address(_pool)), 1); // lender deposit quote tokens in bucket 7388 in order to claim and merge settled collateral and to be able to remove entire NFT - _addLiquidity( - { - from: _lender, - amount: 10 * 1e18, - index: 7388, - lpAward: 9.999999999999999999933756760 * 1e27, // LPs awarded to lender for depositing quote tokens in bucket 7388 - newLup: MAX_PRICE - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: 7388, - lpBalance: 9.999999999999999999933756760 * 1e27, // lender now owns LPs in bucket 7388 which can be used to merge bucket collateral - depositTime: _startTime + 10000 days + (32 hours + 4210 minutes) - } - ); + _addLiquidity({ + from: _lender, + amount: 10 * 1e18, + index: 7388, + lpAward: 10 * 1e18, // LPs awarded to lender for depositing quote tokens in bucket 7388 + newLup: MAX_PRICE + }); + + _assertLenderLpBalance({ + lender: _lender, + index: 7388, + lpBalance: 10 * 1e18, // lender now owns LPs in bucket 7388 which can be used to merge bucket collateral + depositTime: _startTime + 10000 days + (32 hours + 4210 minutes) + }); // collateral is now splitted accross buckets 3069 and 7388 uint256[] memory allRemovalIndexes = new uint256[](2); - allRemovalIndexes[0] = 3069; + allRemovalIndexes[0] = 3070; allRemovalIndexes[1] = 7388; - _mergeOrRemoveCollateral( - { - from: _lender, - toIndex: 7388, - noOfNFTsToRemove: 1, - collateralMerged: 1 * 1e18, - removeCollateralAtIndex: allRemovalIndexes, - toIndexLps: 0 - } - ); + _mergeOrRemoveCollateral({ + from: _lender, + toIndex: 7388, + noOfNFTsToRemove: 1, + collateralMerged: 1 * 1e18, + removeCollateralAtIndex: allRemovalIndexes, + toIndexLps: 0 + }); - _assertBucket( - { - index: 3060, - lpBalance: 0, - collateral: 0, - deposit: 0, - exchangeRate: 1.0 * 1e27 - } - ); - _assertBucket( - { - index: 3061, - lpBalance: 0, - collateral: 0, - deposit: 0, - exchangeRate: 1.0 * 1e27 - } - ); - _assertBucket( - { - index: 3069, - lpBalance: 0, - collateral: 0, - deposit: 0, - exchangeRate: 1.0 * 1e27 - } - ); - _assertBucket( - { - index: 7388, - lpBalance: 10.000000000000000000212666669 * 1e27, // LPs in bucket 7388 diminished when NFT merged and removed - collateral: 0, // no collateral remaining as it was merged and removed - deposit: 10 * 1e18, - exchangeRate: 0.999999999999999999978733333 * 1e27 - } - ); - _assertLenderLpBalance( - { - lender: _lender, - index: 7388, - lpBalance: 9.999999987399196030933756763 * 1e27, // lender LPs decreased with the amount used to merge NFT - depositTime: _startTime + 10000 days + (32 hours + 4210 minutes) - } - ); - _assertLenderLpBalance( - { - lender: _borrower, - index: 7388, - lpBalance: 0.000000012600803969278909906 * 1e27, // Borrower LPs remain the same in the bucket - depositTime: _startTime + 10000 days + (32 hours + 4210 minutes) - } - ); + _assertBucket({ + index: 3060, + lpBalance: 0, + collateral: 0, + deposit: 0, + exchangeRate: 1.0 * 1e18 + }); + _assertBucket({ + index: 3061, + lpBalance: 0, + collateral: 0, + deposit: 0, + exchangeRate: 1.0 * 1e18 + }); + _assertBucket({ + index: 3070, + lpBalance: 0, + collateral: 0, + deposit: 0, + exchangeRate: 1.0 * 1e18 + }); + _assertBucket({ + index: 7388, + lpBalance: 10 * 1e18, // LPs in bucket 7388 diminished when NFT merged and removed + collateral: 0, // no collateral remaining as it was merged and removed + deposit: 10 * 1e18, + exchangeRate: 1 * 1e18 + }); + _assertLenderLpBalance({ + lender: _lender, + index: 7388, + lpBalance: 9.999999987399196031 * 1e18, // lender LPs decreased with the amount used to merge NFT + depositTime: _startTime + 10000 days + (32 hours + 4210 minutes) + }); + _assertLenderLpBalance({ + lender: _borrower, + index: 7388, + lpBalance: 0.000000012600803969 * 1e18, // Borrower LPs remain the same in the bucket + depositTime: _startTime + 10000 days + (32 hours + 4210 minutes) + }); - _removeAllLiquidity( - { - from: _lender, - amount: 9.987201910492245717 * 1e18, - index: 7388, - newLup: MAX_PRICE, - lpRedeem: 9.999999987399196030933756763 * 1e27 - } - ); + _removeAllLiquidity({ + from: _lender, + amount: 9.987201910492245717 * 1e18, + index: 7388, + newLup: MAX_PRICE, + lpRedeem: 9.999999987399196031 * 1e18 + }); - _assertBucket( - { - index: 7388, - lpBalance: 0.000000012600803969278909906 * 1e27, // LPs in bucket 7388 diminished when NFT merged and removed - collateral: 0, // no collateral remaining as it was merged and removed - deposit: 0.000000012600803969 * 1e18, - exchangeRate: 0.999999999977865705499427682 * 1e27 - } - ); + _assertBucket({ + index: 7388, + lpBalance: 0.000000012600803969 * 1e18, // LPs in bucket 7388 diminished when NFT merged and removed + collateral: 0, // no collateral remaining as it was merged and removed + deposit: 0.000000012600803969 * 1e18, + exchangeRate: 1 * 1e18 + }); _assertPool( PoolParams({ @@ -1155,7 +1098,7 @@ contract ERC721PoolCollateralTest is ERC721HelperContract { encumberedCollateral: 0, poolDebt: 0, actualUtilization: 0, - targetUtilization: 3123578486.651416548727612650 * 1e18, + targetUtilization: 2_996_091_127.870826153174895975 * 1e18, minDebtAmount: 0, loans: 0, maxBorrower: address(0), @@ -1167,6 +1110,61 @@ contract ERC721PoolCollateralTest is ERC721HelperContract { assertEq(_collateral.balanceOf(_lender), 2); assertEq(_collateral.balanceOf(_borrower), 50); assertEq(_collateral.balanceOf(address(_pool)), 0); + } +} +contract ERC721SubsetPoolCollateralTest is ERC721PoolCollateralTest { + + function setUp() override external { + _borrower = makeAddr("borrower"); + _borrower2 = makeAddr("borrower2"); + _lender = makeAddr("lender"); + _lender2 = makeAddr("lender2"); + + // deploy subset pool + uint256[] memory subsetTokenIds = new uint256[](5); + subsetTokenIds[0] = 1; + subsetTokenIds[1] = 3; + subsetTokenIds[2] = 5; + subsetTokenIds[3] = 51; + subsetTokenIds[4] = 53; + _pool = _deploySubsetPool(subsetTokenIds); + + _mintAndApproveQuoteTokens(_lender, 200_000 * 1e18); + _mintAndApproveQuoteTokens(_borrower, 100 * 1e18); + + _mintAndApproveCollateralTokens(_borrower, 52); + _mintAndApproveCollateralTokens(_borrower2, 53); + } + + /********************************/ + /*** ERC721 Subset Pool Tests ***/ + /********************************/ + + function testPledgeCollateralNotInSubset() external tearDown { + uint256[] memory tokenIdsToAdd = new uint256[](3); + tokenIdsToAdd[0] = 2; + tokenIdsToAdd[1] = 4; + tokenIdsToAdd[2] = 6; + + // should revert if borrower attempts to add tokens not in the pool subset + _assertPledgeCollateralNotInSubsetRevert({ + from: _borrower, + tokenIds: tokenIdsToAdd + }); + } + + function testRemoveCollateralReverts() external tearDown { + uint256 testIndex = 6248; + uint256[] memory tokenIdsToAdd = new uint256[](2); + tokenIdsToAdd[0] = 3; + tokenIdsToAdd[1] = 5; + + _assertAddCollateralExpiredRevert({ + from: _lender, + tokenIds: tokenIdsToAdd, + index: testIndex, + expiry: block.timestamp - 15 + }); } } diff --git a/tests/forge/ERC721Pool/ERC721PoolFactory.t.sol b/tests/forge/ERC721Pool/ERC721PoolFactory.t.sol index 19539ff7e..b8b5cc45e 100644 --- a/tests/forge/ERC721Pool/ERC721PoolFactory.t.sol +++ b/tests/forge/ERC721Pool/ERC721PoolFactory.t.sol @@ -2,12 +2,13 @@ pragma solidity 0.8.14; import { ERC721HelperContract } from './ERC721DSTestPlus.sol'; -import { NFTCollateralToken, Token } from '../utils/Tokens.sol'; +import { NFTCollateralToken, TokenWithNDecimals } from '../utils/Tokens.sol'; import { ERC721Pool } from 'src/ERC721Pool.sol'; import { ERC721PoolFactory } from 'src/ERC721PoolFactory.sol'; import { IPoolErrors } from 'src/interfaces/pool/commons/IPoolErrors.sol'; import { IPoolFactory } from 'src/interfaces/pool/IPoolFactory.sol'; +import { IERC721PoolFactory } from 'src/interfaces/pool/erc721/IERC721PoolFactory.sol'; contract ERC721PoolFactoryTest is ERC721HelperContract { address internal _NFTCollectionPoolAddress; @@ -23,7 +24,7 @@ contract ERC721PoolFactoryTest is ERC721HelperContract { function setUp() external { _startTime = block.timestamp; _collateral = new NFTCollateralToken(); - _quote = new Token("Quote", "Q"); + _quote = new TokenWithNDecimals("Quote", "Q", 18); // deploy factory _factory = new ERC721PoolFactory(_ajna); @@ -89,24 +90,20 @@ contract ERC721PoolFactoryTest is ERC721HelperContract { function testDeployERC721CollectionPoolWithZeroAddress() external { // should revert if trying to deploy with zero address as collateral - _assertDeployWith0xAddressRevert( - { - poolFactory: address(_factory), - collateral: address(0), - quote: address(_quote), - interestRate: 0.05 * 10**18 - } - ); + _assertDeployWith0xAddressRevert({ + poolFactory: address(_factory), + collateral: address(0), + quote: address(_quote), + interestRate: 0.05 * 10**18 + }); // should revert if trying to deploy with zero address as quote token - _assertDeployWith0xAddressRevert( - { - poolFactory: address(_factory), - collateral: address(_collateral), - quote: address(0), - interestRate: 0.05 * 10**18 - } - ); + _assertDeployWith0xAddressRevert({ + poolFactory: address(_factory), + collateral: address(_collateral), + quote: address(0), + interestRate: 0.05 * 10**18 + }); // check tracking of deployed pools assertEq(_factory.getDeployedPoolsList().length, 3); @@ -115,52 +112,73 @@ contract ERC721PoolFactoryTest is ERC721HelperContract { function testDeployERC721CollectionPoolWithInvalidRate() external { // should revert if trying to deploy with interest rate lower than accepted - _assertDeployWithInvalidRateRevert( - { - poolFactory: address(_factory), - collateral: address(_quote), - quote: address(_quote), - interestRate: 10**18 - } - ); + _assertDeployWithInvalidRateRevert({ + poolFactory: address(_factory), + collateral: address(_quote), + quote: address(_quote), + interestRate: 10**18 + }); // should revert if trying to deploy with interest rate higher than accepted - _assertDeployWithInvalidRateRevert( - { - poolFactory: address(_factory), - collateral: address(_quote), - quote: address(_quote), - interestRate: 2 * 10**18 - } - ); + _assertDeployWithInvalidRateRevert({ + poolFactory: address(_factory), + collateral: address(_quote), + quote: address(_quote), + interestRate: 2 * 10**18 + }); // check tracking of deployed pools assertEq(_factory.getDeployedPoolsList().length, 3); assertEq(_factory.getNumberOfDeployedPools(), 3); } + // FIXME: This test is exceeding block gas limit function testDeployERC721CollectionPoolWithNonNFTAddress() external { // should revert if trying to deploy with non NFT - _assertDeployWithNonNFTRevert( - { - poolFactory: address(_factory), - collateral: address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2), - quote: address(_quote), - interestRate: 0.05 * 10**18 - } - ); + _assertDeployWithNonNFTRevert({ + poolFactory: address(_factory), + collateral: address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2), + quote: address(_quote), + interestRate: 0.05 * 10**18 + }); } function testDeployERC721PoolMultipleTimes() external { // should revert if trying to deploy same pool one more time - _assertDeployMultipleTimesRevert( - { - poolFactory: address(_factory), - collateral: address(_collateral), - quote: address(_quote), - interestRate: 0.05 * 10**18 - } + _assertDeployMultipleTimesRevert({ + poolFactory: address(_factory), + collateral: address(_collateral), + quote: address(_quote), + interestRate: 0.05 * 10**18 + }); + } + + function testDeployERC721PoolWithMinRate() external { + uint256[] memory tokenIds = new uint256[](0); + _factory.deployPool( + address(new NFTCollateralToken()), + address(new TokenWithNDecimals("Quote", "Q1", 18)), + tokenIds, + 0.01 * 10**18 + ); + + // check tracking of deployed pools + assertEq(_factory.getDeployedPoolsList().length, 4); + assertEq(_factory.getNumberOfDeployedPools(), 4); + } + + function testDeployERC721PoolWithMaxRate() external { + uint256[] memory tokenIds = new uint256[](0); + _factory.deployPool( + address(new NFTCollateralToken()), + address(new TokenWithNDecimals("Quote", "Q1", 18)), + tokenIds, + 0.1 * 10**18 ); + + // check tracking of deployed pools + assertEq(_factory.getDeployedPoolsList().length, 4); + assertEq(_factory.getNumberOfDeployedPools(), 4); } function testDeployERC721CollectionPool() external { @@ -188,6 +206,32 @@ contract ERC721PoolFactoryTest is ERC721HelperContract { /*** ERC721 Collection Subset Tests ***/ /**************************************/ + function testGetNFTSubsetHashTokenOrdering() external { + uint256[] memory tokenIdsOne = new uint256[](3); + tokenIdsOne[0] = 1; + tokenIdsOne[1] = 70; + tokenIdsOne[2] = 3; + uint256[] memory tokenIdsTwo = new uint256[](3); + tokenIdsTwo[0] = 1; + tokenIdsTwo[1] = 2; + tokenIdsTwo[2] = 3; + uint256[] memory tokenIdsThree = new uint256[](3); + tokenIdsThree[0] = 1; + tokenIdsThree[1] = 2; + tokenIdsThree[2] = 2; + + // check sort order + vm.expectRevert(IERC721PoolFactory.TokenIdSubsetInvalid.selector); + _factory.getNFTSubsetHash(tokenIdsOne); + + // check valid subset hash + assertEq(_factory.getNFTSubsetHash(tokenIdsTwo), keccak256(abi.encode(tokenIdsTwo))); + + // check for duplicate tokenIds + vm.expectRevert(IERC721PoolFactory.TokenIdSubsetInvalid.selector); + _factory.getNFTSubsetHash(tokenIdsThree); + } + function testDeployERC721SubsetPoolWithZeroAddress() external { uint256[] memory tokenIdsTestSubset = new uint256[](3); tokenIdsTestSubset[0] = 1; diff --git a/tests/forge/ERC721Pool/ERC721FlashloanQuote.t.sol b/tests/forge/ERC721Pool/ERC721PoolFlashloan.t.sol similarity index 89% rename from tests/forge/ERC721Pool/ERC721FlashloanQuote.t.sol rename to tests/forge/ERC721Pool/ERC721PoolFlashloan.t.sol index 0838f8b44..bb0e01d3e 100644 --- a/tests/forge/ERC721Pool/ERC721FlashloanQuote.t.sol +++ b/tests/forge/ERC721Pool/ERC721PoolFlashloan.t.sol @@ -31,32 +31,28 @@ contract ERC721PoolFlashloanTest is ERC721HelperContract { _bucketPrice = 251.186576139566121965 * 1e18; _bucketId = _indexOf(_bucketPrice); assertEq(_bucketId, 3048); - _addInitialLiquidity( - { - from: _lender, - amount: 300 * 1e18, - index: _bucketId - } - ); + + _addInitialLiquidity({ + from: _lender, + amount: 300 * 1e18, + index: _bucketId + }); // borrower draws debt uint256[] memory tokenIdsToAdd = new uint256[](1); tokenIdsToAdd[0] = 1; - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - tokenIds: tokenIdsToAdd - } - ); - _borrow( - { - from: _borrower, - amount: 200 * 1e18, - indexLimit: _bucketId, - newLup: _bucketPrice - } - ); + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + tokenIds: tokenIdsToAdd + }); + _borrow({ + from: _borrower, + amount: 200 * 1e18, + indexLimit: _bucketId, + newLup: _bucketPrice + }); + (uint256 poolDebt,,) = _pool.debtInfo(); assertEq(poolDebt, 200.192307692307692400 * 1e18); } diff --git a/tests/forge/ERC721Pool/ERC721PoolInterest.t.sol b/tests/forge/ERC721Pool/ERC721PoolInterest.t.sol index 806ebb9ee..d1a2df150 100644 --- a/tests/forge/ERC721Pool/ERC721PoolInterest.t.sol +++ b/tests/forge/ERC721Pool/ERC721PoolInterest.t.sol @@ -47,41 +47,32 @@ contract ERC721PoolSubsetInterestTest is ERC721PoolInterestTest { } function testBorrowerInterestCalculation() external tearDown { - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 2550 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 2551 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 2552 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 2553 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 2554 - } - ); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2550 + }); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2551 + }); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2552 + }); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2553 + }); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2554 + }); + (uint256 liquidityAdded, , , , ) = _poolUtils.poolLoansInfo(address(_pool)); skip(10 days); @@ -91,59 +82,54 @@ contract ERC721PoolSubsetInterestTest is ERC721PoolInterestTest { tokenIdsToAdd[0] = 1; tokenIdsToAdd[1] = 3; tokenIdsToAdd[2] = 5; - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - tokenIds: tokenIdsToAdd - } - ); - _borrow( - { - from: _borrower, - amount: 5_000 * 1e18, - indexLimit: 2_551, - newLup: 3_010.892022197881557845 * 1e18 - } - ); + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + tokenIds: tokenIdsToAdd + }); + _borrow({ + from: _borrower, + amount: 5_000 * 1e18, + indexLimit: 2_551, + newLup: 3_010.892022197881557845 * 1e18 + }); + uint256 expectedDebt = 5_004.326923076923075000 * 1e18; (uint256 poolDebt,,) = _pool.debtInfo(); assertEq(poolDebt, expectedDebt); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: expectedDebt, - borrowerCollateral: 3 * 1e18, - borrowert0Np: 1_743.173878205128204457 * 1e18, - borrowerCollateralization: 1.804973217265326249 * 1e18 - } - ); + + _assertBorrower({ + borrower: _borrower, + borrowerDebt: expectedDebt, + borrowerCollateral: 3 * 1e18, + borrowert0Np: 1_743.173878205128204457 * 1e18, + borrowerCollateralization: 1.804973217265326249 * 1e18 + }); + _assertLenderInterest(liquidityAdded, 0); // borrower pledge additional collateral after some time has passed skip(10 days); tokenIdsToAdd = new uint256[](1); tokenIdsToAdd[0] = 51; - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - tokenIds: tokenIdsToAdd - } - ); + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + tokenIds: tokenIdsToAdd + }); expectedDebt = 5_010.500446015624727374 * 1e18; (poolDebt,,) = _pool.debtInfo(); assertEq(poolDebt, expectedDebt); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: expectedDebt, - borrowerCollateral: 4 * 1e18, - borrowert0Np: 1_743.173878205128204457 * 1e18, - borrowerCollateralization: 2.403665705362551645 * 1e18 - } - ); + + _assertBorrower({ + borrower: _borrower, + borrowerDebt: expectedDebt, + borrowerCollateral: 4 * 1e18, + borrowert0Np: 1_743.173878205128204457 * 1e18, + borrowerCollateralization: 2.403665705362551645 * 1e18 + }); + _assertLenderInterest(liquidityAdded, 5.279480966289700000 * 1e18); // borrower pulls some of their collateral after some time has passed @@ -160,41 +146,39 @@ contract ERC721PoolSubsetInterestTest is ERC721PoolInterestTest { expectedDebt = 5_016.063127975675193806 * 1e18; (poolDebt,,) = _pool.debtInfo(); assertEq(poolDebt, expectedDebt); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: expectedDebt, - borrowerCollateral: 3 * 1e18, - borrowert0Np: 1_735.667387820512819845 * 1e18, - borrowerCollateralization: 1.800750077529217167 * 1e18 - } - ); + + _assertBorrower({ + borrower: _borrower, + borrowerDebt: expectedDebt, + borrowerCollateral: 3 * 1e18, + borrowert0Np: 1_735.667387820512819845 * 1e18, + borrowerCollateralization: 1.800750077529217167 * 1e18 + }); + _assertLenderInterest(liquidityAdded, 10.036615877540250000 * 1e18); // borrower borrows some additional quote after some time has passed skip(10 days); - _borrow( - { - from: _borrower, - amount: 1_000 * 1e18, - indexLimit: 3_000, - newLup: 3_010.892022197881557845 * 1e18 - } - ); + _borrow({ + from: _borrower, + amount: 1_000 * 1e18, + indexLimit: 3_000, + newLup: 3_010.892022197881557845 * 1e18 + }); expectedDebt = 6_021.775783320497493092 * 1e18; (poolDebt,,) = _pool.debtInfo(); assertEq(poolDebt, expectedDebt); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: expectedDebt, - borrowerCollateral: 3 * 1e18, - borrowert0Np: 2_073.483875782488916529 * 1e18, - borrowerCollateralization: 1.500002057800446964 * 1e18 - } - ); + + _assertBorrower({ + borrower: _borrower, + borrowerDebt: expectedDebt, + borrowerCollateral: 3 * 1e18, + borrowert0Np: 2_073.483875782488916529 * 1e18, + borrowerCollateralization: 1.500002057800446964 * 1e18 + }); + _assertLenderInterest(liquidityAdded, 14.322580066674600000 * 1e18); // mint additional quote to borrower to enable repayment @@ -202,6 +186,7 @@ contract ERC721PoolSubsetInterestTest is ERC721PoolInterestTest { // borrower repays their loan after some additional time skip(10 days); + _approveAndRepayDebt({ from: _borrower, borrower: _borrower, @@ -214,41 +199,33 @@ contract ERC721PoolSubsetInterestTest is ERC721PoolInterestTest { (poolDebt,,) = _pool.debtInfo(); assertEq(poolDebt, 0); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 0, - borrowerCollateral: 3 * 1e18, - borrowert0Np: 2_073.483875782488916529 * 1e18, - borrowerCollateralization: 1 * 1e18 - } - ); - + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 0, + borrowerCollateral: 3 * 1e18, + borrowert0Np: 2_073.483875782488916529 * 1e18, + borrowerCollateralization: 1 * 1e18 + }); } function testMultipleBorrowerInterestAccumulation() external tearDown { // lender deposits 10000 Quote into 3 buckets - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 2550 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 2551 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 2552 - } - ); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2550 + }); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2551 + }); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2552 + }); + (uint256 liquidityAdded, , , , ) = _poolUtils.poolLoansInfo(address(_pool)); skip(2 hours); @@ -258,81 +235,73 @@ contract ERC721PoolSubsetInterestTest is ERC721PoolInterestTest { tokenIdsToAdd[0] = 1; tokenIdsToAdd[1] = 3; tokenIdsToAdd[2] = 5; - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - tokenIds: tokenIdsToAdd - } - ); + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + tokenIds: tokenIdsToAdd + }); uint256 borrowAmount = 8_000 * 1e18; - _borrow( - { - from: _borrower, - amount: borrowAmount, - indexLimit: 2_551, - newLup: _priceAt(2550) - } - ); + + _borrow({ + from: _borrower, + amount: borrowAmount, + indexLimit: 2_551, + newLup: _priceAt(2550) + }); + uint256 expectedBorrower1Debt = 8_007.692307692307696000 * 1e18; (uint256 poolDebt,,) = _pool.debtInfo(); assertEq(poolDebt, expectedBorrower1Debt); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: expectedBorrower1Debt, - borrowerCollateral: 3 * 1e18, - borrowert0Np: 2_802.692307692307693600 * 1e18, - borrowerCollateralization: 1.127999893042434013 * 1e18 - } - ); + + _assertBorrower({ + borrower: _borrower, + borrowerDebt: expectedBorrower1Debt, + borrowerCollateral: 3 * 1e18, + borrowert0Np: 2_802.692307692307693600 * 1e18, + borrowerCollateralization: 1.127999893042434013 * 1e18 + }); skip(4 hours); // borrower 2 pledges one NFT and takes out a loan with TP around 2750 tokenIdsToAdd = new uint256[](1); tokenIdsToAdd[0] = 53; - _pledgeCollateral( - { - from: _borrower2, - borrower: _borrower2, - tokenIds: tokenIdsToAdd - } - ); + _pledgeCollateral({ + from: _borrower2, + borrower: _borrower2, + tokenIds: tokenIdsToAdd + }); borrowAmount = 2_750 * 1e18; - _borrow( - { - from: _borrower2, - amount: borrowAmount, - indexLimit: 3_000, - newLup: _priceAt(2551) - } - ); + _borrow({ + from: _borrower2, + amount: borrowAmount, + indexLimit: 3_000, + newLup: _priceAt(2551) + }); + expectedBorrower1Debt = 8_007.875133804645608008 * 1e18; uint256 expectedBorrower2Debt = 2_752.644230769230770500 * 1e18; uint256 expectedPoolDebt = 10_760.519364573876378508 * 1e18; (poolDebt,,) = _pool.debtInfo(); assertEq(poolDebt, expectedPoolDebt); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: expectedBorrower1Debt, - borrowerCollateral: 3 * 1e18, - borrowert0Np: 2_802.692307692307693600 * 1e18, - borrowerCollateralization: 1.122362328272840838 * 1e18 - } - ); - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: expectedBorrower2Debt, - borrowerCollateral: 1 * 1e18, - borrowert0Np: 2_904.661507289418461411 * 1e18, - borrowerCollateralization: 1.088376197116173336 * 1e18 - } - ); + + _assertBorrower({ + borrower: _borrower, + borrowerDebt: expectedBorrower1Debt, + borrowerCollateral: 3 * 1e18, + borrowert0Np: 2_802.692307692307693600 * 1e18, + borrowerCollateralization: 1.122362328272840838 * 1e18 + }); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: expectedBorrower2Debt, + borrowerCollateral: 1 * 1e18, + borrowert0Np: 2_904.661507289418461411 * 1e18, + borrowerCollateralization: 1.088376197116173336 * 1e18 + }); + _assertLenderInterest(liquidityAdded, 0.158098662307440000 * 1e18); skip(4 hours); @@ -340,148 +309,133 @@ contract ERC721PoolSubsetInterestTest is ERC721PoolInterestTest { // borrower 3 pledges one NFT and takes out a loan with TP around 2500 tokenIdsToAdd = new uint256[](1); tokenIdsToAdd[0] = 73; - _pledgeCollateral( - { - from: _borrower3, - borrower: _borrower3, - tokenIds: tokenIdsToAdd - } - ); + _pledgeCollateral({ + from: _borrower3, + borrower: _borrower3, + tokenIds: tokenIdsToAdd + }); + borrowAmount = 2_500 * 1e18; - _borrow( - { - from: _borrower3, - amount: borrowAmount, - indexLimit: 3_000, - newLup: _priceAt(2551) - } - ); + _borrow({ + from: _borrower3, + amount: borrowAmount, + indexLimit: 3_000, + newLup: _priceAt(2551) + }); + expectedBorrower1Debt = 8_008.057964091143327677 * 1e18; expectedBorrower2Debt = 2_752.707077245346929053 * 1e18; - uint256 expectedBorrower3Debt = 2_502.403846153846155000 * 1e18; - expectedPoolDebt = 13_263.168887490336411731 * 1e18; + uint256 expectedBorrower3Debt = 2_502.403846153846154999 * 1e18; + expectedPoolDebt = 13_263.168887490336411730 * 1e18; + (poolDebt,,) = _pool.debtInfo(); assertEq(poolDebt, expectedPoolDebt); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: expectedBorrower1Debt, - borrowerCollateral: 3 * 1e18, - borrowert0Np: 2_802.692307692307693600 * 1e18, - borrowerCollateralization: 1.122336703854666979 * 1e18 - } - ); - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: expectedBorrower2Debt, - borrowerCollateral: 1 * 1e18, - borrowert0Np: 2_904.661507289418461411 * 1e18, - borrowerCollateralization: 1.088351348628209297 * 1e18 - } - ); - _assertBorrower( - { - borrower: _borrower3, - borrowerDebt: expectedBorrower3Debt, - borrowerCollateral: 1 * 1e18, - borrowert0Np: 2_640.541083248800813687 * 1e18, - borrowerCollateralization: 1.197213816827790670 * 1e18 - } - ); + + _assertBorrower({ + borrower: _borrower, + borrowerDebt: expectedBorrower1Debt, + borrowerCollateral: 3 * 1e18, + borrowert0Np: 2_802.692307692307693600 * 1e18, + borrowerCollateralization: 1.122336703854666979 * 1e18 + }); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: expectedBorrower2Debt, + borrowerCollateral: 1 * 1e18, + borrowert0Np: 2_904.661507289418461411 * 1e18, + borrowerCollateralization: 1.088351348628209297 * 1e18 + }); + _assertBorrower({ + borrower: _borrower3, + borrowerDebt: expectedBorrower3Debt, + borrowerCollateral: 1 * 1e18, + borrowert0Np: 2_640.541083248800813686 * 1e18, + borrowerCollateralization: 1.197213816827790670 * 1e18 + }); + _assertLenderInterest(liquidityAdded, 0.371995968618930000 * 1e18); skip(4 hours); // trigger an interest accumulation - _addLiquidity( - { - from: _lender, - amount: 1 * 1e18, - index: 2550, - lpAward: 0.999978753161905147653029533 * 1e27, - newLup: 2995.912459898389633881 * 1e18 - } - ); + _addLiquidity({ + from: _lender, + amount: 1 * 1e18, + index: 2550, + lpAward: 0.999978753161905148 * 1e18, + newLup: 2995.912459898389633881 * 1e18 + }); liquidityAdded += 1e18; // check pool and borrower debt to confirm interest has accumulated - expectedPoolDebt = 13_263.471703022178416340 * 1e18; + expectedPoolDebt = 13_263.471703022178416339 * 1e18; + (poolDebt,,) = _pool.debtInfo(); assertEq(poolDebt, expectedPoolDebt); + _assertLenderInterest(liquidityAdded, 0.637418685977190000 * 1e18); expectedBorrower1Debt = 8_008.240798551896146546 * 1e18; - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: expectedBorrower1Debt, - borrowerCollateral: 3 * 1e18, - borrowert0Np: 2_802.692307692307693600 * 1e18, - borrowerCollateralization: 1.122311080021518821 * 1e18 - } - ); + + _assertBorrower({ + borrower: _borrower, + borrowerDebt: expectedBorrower1Debt, + borrowerCollateral: 3 * 1e18, + borrowert0Np: 2_802.692307692307693600 * 1e18, + borrowerCollateralization: 1.122311080021518821 * 1e18 + }); + expectedBorrower2Debt = 2_752.769925156330518052 * 1e18; - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: expectedBorrower2Debt, - borrowerCollateral: 1 * 1e18, - borrowert0Np: 2_904.661507289418461411 * 1e18, - borrowerCollateralization: 1.088326500707555859 * 1e18 - } - ); - expectedBorrower3Debt = 2_502.460979313951751742 * 1e18; - _assertBorrower( - { - borrower: _borrower3, - borrowerDebt: expectedBorrower3Debt, - borrowerCollateral: 1 * 1e18, - borrowert0Np: 2_640.541083248800813687 * 1e18, - borrowerCollateralization: 1.197186483491030227 * 1e18 - } - ); + + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: expectedBorrower2Debt, + borrowerCollateral: 1 * 1e18, + borrowert0Np: 2_904.661507289418461411 * 1e18, + borrowerCollateralization: 1.088326500707555859 * 1e18 + }); + + expectedBorrower3Debt = 2_502.460979313951751741 * 1e18; + + _assertBorrower({ + borrower: _borrower3, + borrowerDebt: expectedBorrower3Debt, + borrowerCollateral: 1 * 1e18, + borrowert0Np: 2_640.541083248800813686 * 1e18, + borrowerCollateralization: 1.197186483491030227 * 1e18 + }); // ensure debt from the three borrowers adds up to the pool debt assertEq(expectedPoolDebt, expectedBorrower1Debt + expectedBorrower2Debt + expectedBorrower3Debt); } function testBorrowerInterestCalculationAfterRepayingAllDebtOnce() external tearDown { - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 2550 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 2551 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 2552 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 2553 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 2554 - } - ); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2550 + }); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2551 + }); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2552 + }); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2553 + }); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2554 + }); + (uint256 liquidityAdded, , , , ) = _poolUtils.poolLoansInfo(address(_pool)); skip(10 days); @@ -491,34 +445,29 @@ contract ERC721PoolSubsetInterestTest is ERC721PoolInterestTest { tokenIdsToAdd[0] = 1; tokenIdsToAdd[1] = 3; tokenIdsToAdd[2] = 5; - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - tokenIds: tokenIdsToAdd - } - ); - _borrow( - { - from: _borrower, - amount: 5_000 * 1e18, - indexLimit: 2_551, - newLup: 3_010.892022197881557845 * 1e18 - } - ); + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + tokenIds: tokenIdsToAdd + }); + _borrow({ + from: _borrower, + amount: 5_000 * 1e18, + indexLimit: 2_551, + newLup: 3_010.892022197881557845 * 1e18 + }); uint256 expectedDebt = 5_004.326923076923075000 * 1e18; (uint256 poolDebt, , ) = _pool.debtInfo(); assertEq(poolDebt, expectedDebt); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: expectedDebt, - borrowerCollateral: 3 * 1e18, - borrowert0Np: 1_743.173878205128204457 * 1e18, - borrowerCollateralization: 1.804973217265326249 * 1e18 - } - ); + + _assertBorrower({ + borrower: _borrower, + borrowerDebt: expectedDebt, + borrowerCollateral: 3 * 1e18, + borrowert0Np: 1_743.173878205128204457 * 1e18, + borrowerCollateralization: 1.804973217265326249 * 1e18 + }); // time passes and interest accrues skip(30 days); @@ -526,15 +475,14 @@ contract ERC721PoolSubsetInterestTest is ERC721PoolInterestTest { expectedDebt = 5_022.870348947539432923 * 1e18; (poolDebt, , ) = _pool.debtInfo(); assertEq(poolDebt, expectedDebt); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: expectedDebt, - borrowerCollateral: 3 * 1e18, - borrowert0Np: 1_743.173878205128204457 * 1e18, - borrowerCollateralization: 1.798309619615464420 * 1e18 - } - ); + + _assertBorrower({ + borrower: _borrower, + borrowerDebt: expectedDebt, + borrowerCollateral: 3 * 1e18, + borrowert0Np: 1_743.173878205128204457 * 1e18, + borrowerCollateralization: 1.798309619615464420 * 1e18 + }); // mint additional quote to borrower to enable repayment deal(address(_quote), _borrower, 20_000 * 1e18); @@ -554,41 +502,37 @@ contract ERC721PoolSubsetInterestTest is ERC721PoolInterestTest { (poolDebt, , ) = _pool.debtInfo(); assertEq(poolDebt, 0); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 0, - borrowerCollateral: 3 * 1e18, - borrowert0Np: 1_743.173878205128204457 * 1e18, - borrowerCollateralization: 1 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 0, + borrowerCollateral: 3 * 1e18, + borrowert0Np: 1_743.173878205128204457 * 1e18, + borrowerCollateralization: 1 * 1e18 + }); + _assertLenderInterest(liquidityAdded, 21.157033792511550000 * 1e18); // borrower borrows again once repayed all debt - _borrow( - { - from: _borrower, - amount: 5_000 * 1e18, - indexLimit: 2_551, - newLup: 3_010.892022197881557845 * 1e18 - } - ); - - expectedDebt = 5_003.894230769230770000 * 1e18; + _borrow({ + from: _borrower, + amount: 5_000 * 1e18, + indexLimit: 2_551, + newLup: 3_010.892022197881557845 * 1e18 + }); + + expectedDebt = 5_003.894230769230769999 * 1e18; (poolDebt, , ) = _pool.debtInfo(); assertEq(poolDebt, expectedDebt); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: expectedDebt, - borrowerCollateral: 3 * 1e18, - borrowert0Np: 1_726.979669209494250313 * 1e18, - borrowerCollateralization: 1.805129295309881815 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: expectedDebt, + borrowerCollateral: 3 * 1e18, + borrowert0Np: 1_726.979669209494250312 * 1e18, + borrowerCollateralization: 1.805129295309881815 * 1e18 + }); + _assertLenderInterest(liquidityAdded, 21.157033792511550000 * 1e18); @@ -596,26 +540,24 @@ contract ERC721PoolSubsetInterestTest is ERC721PoolInterestTest { skip(10 days); tokenIdsToAdd = new uint256[](1); tokenIdsToAdd[0] = 51; - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - tokenIds: tokenIdsToAdd - } - ); - - expectedDebt = 5_009.449578476990224066 * 1e18; + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + tokenIds: tokenIdsToAdd + }); + + expectedDebt = 5_009.449578476990224065 * 1e18; (poolDebt, , ) = _pool.debtInfo(); assertEq(poolDebt, expectedDebt); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: expectedDebt, - borrowerCollateral: 4 * 1e18, - borrowert0Np: 1_726.979669209494250313 * 1e18, - borrowerCollateralization: 2.404169939255701731 * 1e18 - } - ); + + _assertBorrower({ + borrower: _borrower, + borrowerDebt: expectedDebt, + borrowerCollateral: 4 * 1e18, + borrowert0Np: 1_726.979669209494250312 * 1e18, + borrowerCollateralization: 2.404169939255701731 * 1e18 + }); + _assertLenderInterest(liquidityAdded, 25.907847709272800000 * 1e18); // borrower pulls some of their collateral after some time has passed @@ -629,44 +571,42 @@ contract ERC721PoolSubsetInterestTest is ERC721PoolInterestTest { collateralToPull: 1 }); - expectedDebt = 5_014.454664494689841710 * 1e18; + expectedDebt = 5_014.454664494689841709 * 1e18; (poolDebt, , ) = _pool.debtInfo(); assertEq(poolDebt, expectedDebt); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: expectedDebt, - borrowerCollateral: 3 * 1e18, - borrowert0Np: 1_720.257643586910442803 * 1e18, - borrowerCollateralization: 1.801327695821111558 * 1e18 - } - ); + + _assertBorrower({ + borrower: _borrower, + borrowerDebt: expectedDebt, + borrowerCollateral: 3 * 1e18, + borrowert0Np: 1_720.257643586910442802 * 1e18, + borrowerCollateralization: 1.801327695821111558 * 1e18 + }); + _assertLenderInterest(liquidityAdded, 30.188116923725550000 * 1e18); // borrower borrows some additional quote after some time has passed skip(10 days); - _borrow( - { - from: _borrower, - amount: 1_000 * 1e18, - indexLimit: 3_000, - newLup: 3_010.892022197881557845 * 1e18 - } - ); + _borrow({ + from: _borrower, + amount: 1_000 * 1e18, + indexLimit: 3_000, + newLup: 3_010.892022197881557845 * 1e18 + }); - expectedDebt = 6_019.594382773827921758 * 1e18; + expectedDebt = 6_019.594382773827921756 * 1e18; (poolDebt, , ) = _pool.debtInfo(); assertEq(poolDebt, expectedDebt); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: expectedDebt, - borrowerCollateral: 3 * 1e18, - borrowert0Np: 2_055.969470907112040316 * 1e18, - borrowerCollateralization: 1.500545633513497515 * 1e18 - } - ); + + _assertBorrower({ + borrower: _borrower, + borrowerDebt: expectedDebt, + borrowerCollateral: 3 * 1e18, + borrowert0Np: 2_055.969470907112040315 * 1e18, + borrowerCollateralization: 1.500545633513497515 * 1e18 + }); + _assertLenderInterest(liquidityAdded, 34.044037663170400000 * 1e18); // mint additional quote to borrower to enable repayment @@ -679,7 +619,7 @@ contract ERC721PoolSubsetInterestTest is ERC721PoolInterestTest { from: _borrower, borrower: _borrower, amountToRepay: 7_000 * 1e18, - amountRepaid: 6_024.465544800440916672 * 1e18, + amountRepaid: 6_024.465544800440916669 * 1e18, collateralToPull: 0, newLup: MAX_PRICE }); @@ -687,15 +627,14 @@ contract ERC721PoolSubsetInterestTest is ERC721PoolInterestTest { (poolDebt, , ) = _pool.debtInfo(); assertEq(poolDebt, 0); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 0, - borrowerCollateral: 3 * 1e18, - borrowert0Np: 2_055.969470907112040316 * 1e18, - borrowerCollateralization: 1 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 0, + borrowerCollateral: 3 * 1e18, + borrowert0Np: 2_055.969470907112040315 * 1e18, + borrowerCollateralization: 1 * 1e18 + }); + _assertLenderInterest(liquidityAdded, 38.215088437572850000 * 1e18); } } \ No newline at end of file diff --git a/tests/forge/ERC721Pool/ERC721PoolLiquidationsDepositTake.t.sol b/tests/forge/ERC721Pool/ERC721PoolLiquidationsDepositTake.t.sol new file mode 100644 index 000000000..341bdb1d9 --- /dev/null +++ b/tests/forge/ERC721Pool/ERC721PoolLiquidationsDepositTake.t.sol @@ -0,0 +1,308 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.14; + +import { ERC721HelperContract } from './ERC721DSTestPlus.sol'; + +import 'src/libraries/helpers/PoolHelper.sol'; + +contract ERC721PoolLiquidationsDepositTakeTest is ERC721HelperContract { + + address internal _borrower; + address internal _borrower2; + address internal _lender; + address internal _lender2; + address internal _taker; + + function setUp() external { + _borrower = makeAddr("borrower"); + _borrower2 = makeAddr("borrower2"); + _lender = makeAddr("lender"); + _lender2 = makeAddr("lender2"); + _taker = makeAddr("taker"); + + // deploy subset pool + uint256[] memory subsetTokenIds = new uint256[](6); + subsetTokenIds[0] = 1; + subsetTokenIds[1] = 3; + subsetTokenIds[2] = 5; + subsetTokenIds[3] = 51; + subsetTokenIds[4] = 53; + subsetTokenIds[5] = 73; + _pool = _deploySubsetPool(subsetTokenIds); + + _mintAndApproveQuoteTokens(_lender, 120_000 * 1e18); + _mintAndApproveQuoteTokens(_lender2, 120_000 * 1e18); + _mintAndApproveQuoteTokens(_borrower, 10 * 1e18); + + _mintAndApproveCollateralTokens(_borrower, 6); + _mintAndApproveCollateralTokens(_borrower2, 74); + + // Lender adds Quote token accross 5 prices + _addInitialLiquidity({ + from: _lender, + amount: 2_000 * 1e18, + index: _i9_91 + }); + _addInitialLiquidity({ + from: _lender, + amount: 5_000 * 1e18, + index: _i9_81 + }); + _addInitialLiquidity({ + from: _lender, + amount: 11_000 * 1e18, + index: _i9_72 + }); + _addInitialLiquidity({ + from: _lender, + amount: 25_000 * 1e18, + index: _i9_62 + }); + _addInitialLiquidity({ + from: _lender, + amount: 30_000 * 1e18, + index: _i9_52 + }); + + // first borrower adds collateral token and borrows + uint256[] memory tokenIdsToAdd = new uint256[](2); + tokenIdsToAdd[0] = 1; + tokenIdsToAdd[1] = 3; + + uint256 expectedNewLup = 9.917184843435912074 * 1e18; + + // borrower deposits two NFTs into the subset pool and borrows + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + tokenIds: tokenIdsToAdd + }); + _borrow({ + from: _borrower, + amount: 19.8 * 1e18, + indexLimit: _i9_91, + newLup: expectedNewLup + }); + + // second borrower deposits three NFTs into the subset pool and borrows + tokenIdsToAdd = new uint256[](3); + tokenIdsToAdd[0] = 51; + tokenIdsToAdd[1] = 53; + tokenIdsToAdd[2] = 73; + _pledgeCollateral({ + from: _borrower2, + borrower: _borrower2, + tokenIds: tokenIdsToAdd + }); + _borrow({ + from: _borrower2, + amount: 15 * 1e18, + indexLimit: _i9_72, + newLup: expectedNewLup + }); + + /*****************************/ + /*** Assert pre-kick state ***/ + /*****************************/ + + _assertPool( + PoolParams({ + htp: 9.909519230769230774 * 1e18, + lup: expectedNewLup, + poolSize: 73_000 * 1e18, + pledgedCollateral: 5 * 1e18, + encumberedCollateral: 3.512434434608473285 * 1e18, + poolDebt: 34.833461538461538478 * 1e18, + actualUtilization: 0.000477170706006322 * 1e18, + targetUtilization: 1 * 1e18, + minDebtAmount: 1.741673076923076924 * 1e18, + loans: 2, + maxBorrower: address(_borrower), + interestRate: 0.05 * 1e18, + interestRateUpdate: _startTime + }) + ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 19.819038461538461548 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 10.404995192307692312 * 1e18, + borrowerCollateralization: 1.000773560501591181 * 1e18 + }); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 15.014423076923076930 * 1e18, + borrowerCollateral: 3 * 1e18, + borrowert0Np: 5.255048076923076925 * 1e18, + borrowerCollateralization: 1.981531649793150539 * 1e18 + }); + + assertEq(_quote.balanceOf(_lender), 47_000 * 1e18); + + // Skip to make borrower undercollateralized + skip(1000 days); + + _kick({ + from: _lender, + borrower: _borrower, + debt: 23.012828827714740289 * 1e18, + collateral: 2 * 1e18, + bond: 0.227287198298417188 * 1e18, + transferAmount: 0.227287198298417188 * 1e18 + }); + + /******************************/ + /*** Assert Post-kick state ***/ + /******************************/ + + _assertPool( + PoolParams({ + htp: 6.582216822103492762 * 1e18, + lup: 9.917184843435912074 * 1e18, + poolSize: 73_000 * 1e18, + pledgedCollateral: 5 * 1e18, + encumberedCollateral: 4.056751649452525709 * 1e18, + poolDebt: 40.231555971534224231 * 1e18, + actualUtilization: 0.000551117205089510 * 1e18, + targetUtilization: 0.811350329890505142 * 1e18, + minDebtAmount: 4.023155597153422423 * 1e18, + loans: 1, + maxBorrower: address(_borrower2), + interestRate: 0.045 * 1e18, + interestRateUpdate: block.timestamp + }) + ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 23.012828827714740289 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 10.404995192307692312 * 1e18, + borrowerCollateralization: 0.861883162446546169 * 1e18 + }); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 17.218727143819483942 * 1e18, + borrowerCollateral: 3 * 1e18, + borrowert0Np: 5.255048076923076925 * 1e18, + borrowerCollateralization: 1.727860269914713433 * 1e18 + }); + } + + function testDepositTakeNFTAndSettleAuction() external { + + skip(5 hours); + + _assertAuction( + AuctionParams({ + borrower: _borrower, + active: true, + kicker: _lender, + bondSize: 0.227287198298417188 * 1e18, + bondFactor: 0.01 * 1e18, + kickTime: block.timestamp - 5 hours, + kickMomp: 9.917184843435912074 * 1e18, + totalBondEscrowed: 0.227287198298417188 * 1e18, + auctionPrice: 23.865155821333804736 * 1e18, + debtInAuction: 23.012828827714740289 * 1e18, + thresholdPrice: 11.506709959118993144 * 1e18, + neutralPrice: 11.932577910666902372 * 1e18 + }) + ); + + _addLiquidity({ + from: _lender, + amount: 15.0 * 1e18, + index: _i1505_26, + lpAward: 15.0 * 1e18, + newLup: 9.917184843435912074 * 1e18 + }); + + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 23.013419918237986289 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 10.404995192307692312 * 1e18, + borrowerCollateralization: 0.861861025320848319 * 1e18 + }); + + // before deposit take: NFTs pledged by auctioned borrower are owned by the pool + assertEq(_collateral.ownerOf(3), address(_pool)); + assertEq(_collateral.ownerOf(1), address(_pool)); + + _depositTake({ + from: _taker, + borrower: _borrower, + kicker: _lender, + index: _i1505_26, + collateralArbed: 0.009965031187761219 * 1e18, + quoteTokenAmount: 14.999999999999999995 * 1e18, + bondChange: 0.15 * 1e18, + isReward: false, + lpAwardTaker: 0, + lpAwardKicker: 0 + }); + + _assertAuction( + AuctionParams({ + borrower: _borrower, + active: false, + kicker: address(0), + bondSize: 0, + bondFactor: 0, + kickTime: 0, + kickMomp: 0, + totalBondEscrowed: 0, + auctionPrice: 0, + debtInAuction: 0, + thresholdPrice: 9.624359312514645329 * 1e18, + neutralPrice: 0 + }) + ); + // borrower is compensated LPs for fractional collateral + _assertLenderLpBalance({ + lender: _borrower, + index: 3519, + lpBalance: 23.737330323739529015 * 1e18, + depositTime: block.timestamp + }); + _assertBucket({ + index: _i1505_26, + lpBalance: 15 * 1e18, + collateral: 0.009965031187761219 * 1e18, + deposit: 0, + exchangeRate: 1 * 1e18 + }); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 9.624359312514645329 * 1e18, + borrowerCollateral: 1 * 1e18, + borrowert0Np: 8.769696613728507377 * 1e18, + borrowerCollateralization: 1.030425457052554443 * 1e18 + }); + _assertLenderLpBalance({ + lender: _taker, + index: _i1505_26, + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: _lender, + index: _i1505_26, + lpBalance: 15.0 * 1e18, + depositTime: block.timestamp + }); + + // borrower should be able to repay and pull collateral from the pool + _repayDebtNoLupCheck({ + from: _borrower, + borrower: _borrower, + amountToRepay: 10 * 1e18, + amountRepaid: 10 * 1e18, + collateralToPull: 1 + }); + + // after deposit take and pull: NFT taken remains in pool, the pulled one goes to borrower + assertEq(_collateral.ownerOf(3), address(_pool)); + assertEq(_collateral.ownerOf(1), _borrower); + } +} diff --git a/tests/forge/ERC721Pool/ERC721PoolLiquidationsKick.t.sol b/tests/forge/ERC721Pool/ERC721PoolLiquidationsKick.t.sol index c56487125..956703212 100644 --- a/tests/forge/ERC721Pool/ERC721PoolLiquidationsKick.t.sol +++ b/tests/forge/ERC721Pool/ERC721PoolLiquidationsKick.t.sol @@ -34,84 +34,65 @@ contract ERC721PoolLiquidationsKickTest is ERC721HelperContract { _mintAndApproveCollateralTokens(_borrower2, 74); // Lender adds Quote token accross 5 prices - _addInitialLiquidity( - { - from: _lender, - amount: 2_000 * 1e18, - index: _i9_91 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 5_000 * 1e18, - index: _i9_81 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 11_000 * 1e18, - index: _i9_72 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 25_000 * 1e18, - index: _i9_62 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 30_000 * 1e18, - index: _i9_52 - } - ); + _addInitialLiquidity({ + from: _lender, + amount: 2_000 * 1e18, + index: _i9_91 + }); + _addInitialLiquidity({ + from: _lender, + amount: 5_000 * 1e18, + index: _i9_81 + }); + _addInitialLiquidity({ + from: _lender, + amount: 11_000 * 1e18, + index: _i9_72 + }); + _addInitialLiquidity({ + from: _lender, + amount: 25_000 * 1e18, + index: _i9_62 + }); + _addInitialLiquidity({ + from: _lender, + amount: 30_000 * 1e18, + index: _i9_52 + }); // first borrower adds collateral token and borrows uint256[] memory tokenIdsToAdd = new uint256[](2); tokenIdsToAdd[0] = 1; tokenIdsToAdd[1] = 3; - // borrower deposits two NFTs into the subset pool and borrows - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - tokenIds: tokenIdsToAdd - } - ); - _borrow( - { - from: _borrower, - amount: 19.8 * 1e18, - indexLimit: _i9_91, - newLup: 9.917184843435912074 * 1e18 - } - ); + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + tokenIds: tokenIdsToAdd + }); + _borrow({ + from: _borrower, + amount: 19.8 * 1e18, + indexLimit: _i9_91, + newLup: 9.917184843435912074 * 1e18 + }); // second borrower deposits three NFTs into the subset pool and borrows tokenIdsToAdd = new uint256[](3); tokenIdsToAdd[0] = 51; tokenIdsToAdd[1] = 53; tokenIdsToAdd[2] = 73; - _pledgeCollateral( - { - from: _borrower2, - borrower: _borrower2, - tokenIds: tokenIdsToAdd - } - ); - _borrow( - { - from: _borrower2, - amount: 15 * 1e18, - indexLimit: _i9_72, - newLup: 9.917184843435912074 * 1e18 - } - ); + _pledgeCollateral({ + from: _borrower2, + borrower: _borrower2, + tokenIds: tokenIdsToAdd + }); + _borrow({ + from: _borrower2, + amount: 15 * 1e18, + indexLimit: _i9_72, + newLup: 9.917184843435912074 * 1e18 + }); /*****************************/ /*** Assert pre-kick state ***/ @@ -134,24 +115,21 @@ contract ERC721PoolLiquidationsKickTest is ERC721HelperContract { interestRateUpdate: _startTime }) ); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 19.819038461538461548 * 1e18, - borrowerCollateral: 2 * 1e18, - borrowert0Np: 10.404995192307692312 * 1e18, - borrowerCollateralization: 1.000773560501591181 * 1e18 - } - ); - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 15.014423076923076930 * 1e18, - borrowerCollateral: 3 * 1e18, - borrowert0Np: 5.255048076923076925 * 1e18, - borrowerCollateralization: 1.981531649793150539 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 19.819038461538461548 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 10.404995192307692312 * 1e18, + borrowerCollateralization: 1.000773560501591181 * 1e18 + }); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 15.014423076923076930 * 1e18, + borrowerCollateral: 3 * 1e18, + borrowert0Np: 5.255048076923076925 * 1e18, + borrowerCollateralization: 1.981531649793150539 * 1e18 + }); + assertEq(_quote.balanceOf(_lender), 47_000 * 1e18); } @@ -176,27 +154,22 @@ contract ERC721PoolLiquidationsKickTest is ERC721HelperContract { neutralPrice: 0 }) ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 22.728719829841718804 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 10.404995192307692312 * 1e18, + borrowerCollateralization: 0.872656701977127996 * 1e18 + }); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 22.728719829841718804 * 1e18, - borrowerCollateral: 2 * 1e18, - borrowert0Np: 10.404995192307692312 * 1e18, - borrowerCollateralization: 0.872656701977127996 * 1e18 - } - ); - - _kick( - { - from: _lender, - borrower: _borrower, - debt: 23.012828827714740289 * 1e18, - collateral: 2 * 1e18, - bond: 0.227287198298417188 * 1e18, - transferAmount: 0.227287198298417188 * 1e18 - } - ); + _kick({ + from: _lender, + borrower: _borrower, + debt: 23.012828827714740289 * 1e18, + collateral: 2 * 1e18, + bond: 0.227287198298417188 * 1e18, + transferAmount: 0.227287198298417188 * 1e18 + }); /******************************/ /*** Assert Post-kick state ***/ @@ -219,25 +192,6 @@ contract ERC721PoolLiquidationsKickTest is ERC721HelperContract { interestRateUpdate: block.timestamp }) ); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 23.012828827714740289 * 1e18, - borrowerCollateral: 2 * 1e18, - borrowert0Np: 10.404995192307692312 * 1e18, - borrowerCollateralization: 0.861883162446546169 * 1e18 - } - ); - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 17.218727143819483942 * 1e18, - borrowerCollateral: 3 * 1e18, - borrowert0Np: 5.255048076923076925 * 1e18, - borrowerCollateralization: 1.727860269914713433 * 1e18 - } - ); - assertEq(_quote.balanceOf(_lender), 46_999.772712801701582812 * 1e18); _assertAuction( AuctionParams({ borrower: _borrower, @@ -254,46 +208,53 @@ contract ERC721PoolLiquidationsKickTest is ERC721HelperContract { neutralPrice: 11.932577910666902372 * 1e18 }) ); - _assertKicker( - { - kicker: _lender, - claimable: 0, - locked: 0.227287198298417188 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 23.012828827714740289 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 10.404995192307692312 * 1e18, + borrowerCollateralization: 0.861883162446546169 * 1e18 + }); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 17.218727143819483942 * 1e18, + borrowerCollateral: 3 * 1e18, + borrowert0Np: 5.255048076923076925 * 1e18, + borrowerCollateralization: 1.727860269914713433 * 1e18 + }); + _assertKicker({ + kicker: _lender, + claimable: 0, + locked: 0.227287198298417188 * 1e18 + }); + + assertEq(_quote.balanceOf(_lender), 46_999.772712801701582812 * 1e18); // kick should fail if borrower in liquidation - _assertKickAuctionActiveRevert( - { - from: _lender, - borrower: _borrower - } - ); + _assertKickAuctionActiveRevert({ + from: _lender, + borrower: _borrower + }); // kick should fail if borrower properly collateralized - _assertKickCollateralizedBorrowerRevert( - { - from: _lender, - borrower: _borrower2 - } - ); + _assertKickCollateralizedBorrowerRevert({ + from: _lender, + borrower: _borrower2 + }); // check locked pool actions if auction kicked for more than 72 hours and auction head not cleared skip(80 hours); - _assertRemoveLiquidityAuctionNotClearedRevert( - { - from: _lender, - amount: 1_000 * 1e18, - index: _i9_91 - } - ); - _assertRemoveCollateralAuctionNotClearedRevert( - { - from: _lender, - amount: 10 * 1e18, - index: _i9_91 - } - ); + _assertRemoveLiquidityAuctionNotClearedRevert({ + from: _lender, + amount: 1_000 * 1e18, + index: _i9_91 + }); + + _assertRemoveCollateralAuctionNotClearedRevert({ + from: _lender, + amount: 10 * 1e18, + index: _i9_91 + }); } function testKickSubsetPoolAndSettleByRepayAndPledge() external tearDown { @@ -316,26 +277,23 @@ contract ERC721PoolLiquidationsKickTest is ERC721HelperContract { neutralPrice: 0 }) ); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 22.728719829841718804 * 1e18, - borrowerCollateral: 2 * 1e18, - borrowert0Np: 10.404995192307692312 * 1e18, - borrowerCollateralization: 0.872656701977127996 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 22.728719829841718804 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 10.404995192307692312 * 1e18, + borrowerCollateralization: 0.872656701977127996 * 1e18 + }); + + _kick({ + from: _lender, + borrower: _borrower, + debt: 23.012828827714740289 * 1e18, + collateral: 2 * 1e18, + bond: 0.227287198298417188 * 1e18, + transferAmount: 0.227287198298417188 * 1e18 + }); - _kick( - { - from: _lender, - borrower: _borrower, - debt: 23.012828827714740289 * 1e18, - collateral: 2 * 1e18, - bond: 0.227287198298417188 * 1e18, - transferAmount: 0.227287198298417188 * 1e18 - } - ); _assertAuction( AuctionParams({ borrower: _borrower, @@ -352,17 +310,16 @@ contract ERC721PoolLiquidationsKickTest is ERC721HelperContract { neutralPrice: 11.932577910666902372 * 1e18 }) ); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 23.012828827714740289 * 1e18, - borrowerCollateral: 2 * 1e18, - borrowert0Np: 10.404995192307692312 * 1e18, - borrowerCollateralization: 0.861883162446546169 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 23.012828827714740289 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 10.404995192307692312 * 1e18, + borrowerCollateralization: 0.861883162446546169 * 1e18 + }); uint256 snapshot = vm.snapshot(); + // borrower repays debt in order to exit from auction _repayDebt({ from: _borrower, @@ -389,27 +346,24 @@ contract ERC721PoolLiquidationsKickTest is ERC721HelperContract { neutralPrice: 0 }) ); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 0, - borrowerCollateral: 2 * 1e18, - borrowert0Np: 0, - borrowerCollateralization: 1 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 0, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 0, + borrowerCollateralization: 1 * 1e18 + }); + vm.revertTo(snapshot); // borrower pledge one more NFT to exit from auction uint256[] memory tokenIdsToAdd = new uint256[](1); tokenIdsToAdd[0] = 5; - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - tokenIds: tokenIdsToAdd - } - ); + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + tokenIds: tokenIdsToAdd + }); _assertAuction( AuctionParams({ @@ -427,15 +381,13 @@ contract ERC721PoolLiquidationsKickTest is ERC721HelperContract { neutralPrice: 0 }) ); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 23.012828827714740289 * 1e18, - borrowerCollateral: 3 * 1e18, - borrowert0Np: 6.989927127403846156 * 1e18, - borrowerCollateralization: 1.292824743669819254 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 23.012828827714740289 * 1e18, + borrowerCollateral: 3 * 1e18, + borrowert0Np: 6.989927127403846156 * 1e18, + borrowerCollateralization: 1.292824743669819254 * 1e18 + }); } } \ No newline at end of file diff --git a/tests/forge/ERC721Pool/ERC721PoolLiquidationsSettle.t.sol b/tests/forge/ERC721Pool/ERC721PoolLiquidationsSettle.t.sol index 48f3bf1c9..18010fd62 100644 --- a/tests/forge/ERC721Pool/ERC721PoolLiquidationsSettle.t.sol +++ b/tests/forge/ERC721Pool/ERC721PoolLiquidationsSettle.t.sol @@ -34,36 +34,29 @@ contract ERC721PoolLiquidationsSettleTest is ERC721HelperContract { _mintAndApproveCollateralTokens(_borrower2, 74); // Lender adds Quote token in one bucket - _addInitialLiquidity( - { - from: _lender, - amount: 15_000 * 1e18, - index: 2500 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 1_000 * 1e18, - index: 2501 - } - ); + _addInitialLiquidity({ + from: _lender, + amount: 15_000 * 1e18, + index: 2500 + }); + _addInitialLiquidity({ + from: _lender, + amount: 1_000 * 1e18, + index: 2501 + }); // first borrower adds collateral token and borrows uint256[] memory tokenIdsToAdd = new uint256[](2); tokenIdsToAdd[0] = 1; tokenIdsToAdd[1] = 3; - // borrower deposits two NFTs into the subset pool and borrows - _drawDebtNoLupCheck( - { - from: _borrower, - borrower: _borrower, - amountToBorrow: 5_000 * 1e18, - limitIndex: 5000, - tokenIds: tokenIdsToAdd - } - ); + _drawDebtNoLupCheck({ + from: _borrower, + borrower: _borrower, + amountToBorrow: 5_000 * 1e18, + limitIndex: 5000, + tokenIds: tokenIdsToAdd + }); // second borrower deposits three NFTs into the subset pool and borrows tokenIdsToAdd = new uint256[](3); @@ -71,15 +64,13 @@ contract ERC721PoolLiquidationsSettleTest is ERC721HelperContract { tokenIdsToAdd[1] = 53; tokenIdsToAdd[2] = 73; // borrower deposits two NFTs into the subset pool and borrows - _drawDebtNoLupCheck( - { - from: _borrower2, - borrower: _borrower2, - amountToBorrow: 5_000 * 1e18, - limitIndex: 5000, - tokenIds: tokenIdsToAdd - } - ); + _drawDebtNoLupCheck({ + from: _borrower2, + borrower: _borrower2, + amountToBorrow: 5_000 * 1e18, + limitIndex: 5000, + tokenIds: tokenIdsToAdd + }); /*****************************/ /*** Assert pre-kick state ***/ @@ -102,24 +93,21 @@ contract ERC721PoolLiquidationsSettleTest is ERC721HelperContract { interestRateUpdate: _startTime }) ); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 5_004.807692307692310000 * 1e18, - borrowerCollateral: 2 * 1e18, - borrowert0Np: 2_627.524038461538462750 * 1e18, - borrowerCollateralization: 1.543977154129479546 * 1e18 - } - ); - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 5_004.807692307692310000 * 1e18, - borrowerCollateral: 3 * 1e18, - borrowert0Np: 1_751.682692307692308500 * 1e18, - borrowerCollateralization: 2.315965731194219318 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 5_004.807692307692310000 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 2_627.524038461538462750 * 1e18, + borrowerCollateralization: 1.543977154129479546 * 1e18 + }); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 5_004.807692307692310000 * 1e18, + borrowerCollateral: 3 * 1e18, + borrowert0Np: 1_751.682692307692308500 * 1e18, + borrowerCollateralization: 2.315965731194219318 * 1e18 + }); + assertEq(_quote.balanceOf(address(_pool)), 6_000 * 1e18); assertEq(_quote.balanceOf(_lender), 104_000 * 1e18); assertEq(_quote.balanceOf(_borrower), 5_100 * 1e18); @@ -130,34 +118,31 @@ contract ERC721PoolLiquidationsSettleTest is ERC721HelperContract { /*** Kick both loans ***/ /***********************/ - _kickWithDeposit( - { - from: _lender, - index: 2500, - borrower: _borrower, - debt: 5_067.367788461538463875 * 1e18, - collateral: 2 * 1e18, - bond: 1_501.442307692307693000 * 1e18, - removedFromDeposit: 1_501.442307692307693000 * 1e18, - transferAmount: 0, - lup: 3_863.654368867279344664 * 1e18 - } - ); - _kickWithDeposit( - { - from: _lender, - index: 2500, - borrower: _borrower2, - debt: 5_067.367788461538463875 * 1e18, - collateral: 3 * 1e18, - bond: 1_501.442307692307693000 * 1e18, - removedFromDeposit: 1_501.442307692307693000 * 1e18, - transferAmount: 0, - lup: 3_863.654368867279344664 * 1e18 - } - ); + _kickWithDeposit({ + from: _lender, + index: 2500, + borrower: _borrower, + debt: 5_067.367788461538463875 * 1e18, + collateral: 2 * 1e18, + bond: 1_501.442307692307693000 * 1e18, + removedFromDeposit: 1_501.442307692307693000 * 1e18, + transferAmount: 0, + lup: 3_863.654368867279344664 * 1e18 + }); - // skip to make loans clearable + _kickWithDeposit({ + from: _lender, + index: 2500, + borrower: _borrower2, + debt: 5_067.367788461538463875 * 1e18, + collateral: 3 * 1e18, + bond: 1_501.442307692307693000 * 1e18, + removedFromDeposit: 1_501.442307692307693000 * 1e18, + transferAmount: 0, + lup: 3_863.654368867279344664 * 1e18 + }); + + // skip to make loans clearable skip(80 hours); _assertPool( PoolParams({ @@ -176,24 +161,21 @@ contract ERC721PoolLiquidationsSettleTest is ERC721HelperContract { interestRateUpdate: _startTime }) ); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 5_069.682183392068152308 * 1e18, - borrowerCollateral: 2 * 1e18, - borrowert0Np: 2_627.524038461538462750 * 1e18, - borrowerCollateralization: 1.524219558190194493 * 1e18 - } - ); - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 5_069.682183392068152308 * 1e18, - borrowerCollateral: 3 * 1e18, - borrowert0Np: 1_751.682692307692308500 * 1e18, - borrowerCollateralization: 2.286329337285291739 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 5_069.682183392068152308 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 2_627.524038461538462750 * 1e18, + borrowerCollateralization: 1.524219558190194493 * 1e18 + }); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 5_069.682183392068152308 * 1e18, + borrowerCollateral: 3 * 1e18, + borrowert0Np: 1_751.682692307692308500 * 1e18, + borrowerCollateralization: 2.286329337285291739 * 1e18 + }); + assertEq(_quote.balanceOf(address(_pool)), 6_000 * 1e18); assertEq(_quote.balanceOf(_lender), 104_000 * 1e18); assertEq(_quote.balanceOf(_borrower), 5_100 * 1e18); @@ -220,14 +202,22 @@ contract ERC721PoolLiquidationsSettleTest is ERC721HelperContract { neutralPrice: 1_751.682692307692308500 * 1e18 }) ); - _settle( - { - from: _lender, - borrower: _borrower2, - maxDepth: 1, - settledDebt: 5_067.367788461538463875 * 1e18 - } - ); + + // revert if auction is not settled + _assertMergeRemoveCollateralAuctionNotClearedRevert({ + from: _lender, + toIndex: MAX_FENWICK_INDEX, + noOfNFTsToRemove: 2, + removeCollateralAtIndex: new uint256[](0) + }); + + _settle({ + from: _lender, + borrower: _borrower2, + maxDepth: 1, + settledDebt: 5_067.367788461538463875 * 1e18 + }); + _assertAuction( AuctionParams({ borrower: _borrower2, @@ -245,31 +235,21 @@ contract ERC721PoolLiquidationsSettleTest is ERC721HelperContract { }) ); + // revert if auction is not settled + _assertMergeRemoveCollateralAuctionNotClearedRevert({ + from: _lender, + toIndex: MAX_FENWICK_INDEX, + noOfNFTsToRemove: 2, + removeCollateralAtIndex: new uint256[](0) + }); + // settle borrower - _settle( - { - from: _lender, - borrower: _borrower, - maxDepth: 5, - settledDebt: 5_067.367788461538463875 * 1e18 - } - ); - _assertAuction( - AuctionParams({ - borrower: _borrower, - active: false, - kicker: address(0), - bondSize: 0, - bondFactor: 0, - kickTime: 0, - kickMomp: 0, - totalBondEscrowed: 0, - auctionPrice: 0, - debtInAuction: 0, - thresholdPrice: 0, - neutralPrice: 0 - }) - ); + _settle({ + from: _lender, + borrower: _borrower, + maxDepth: 5, + settledDebt: 5_067.367788461538463875 * 1e18 + }); _assertPool( PoolParams({ @@ -288,77 +268,85 @@ contract ERC721PoolLiquidationsSettleTest is ERC721HelperContract { interestRateUpdate: _startTime + 80 hours }) ); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 0, - borrowerCollateral: 0, - borrowert0Np: 2_627.524038461538462750 * 1e18, - borrowerCollateralization: 1 * 1e18 - } - ); - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 0, - borrowerCollateral: 1 * 1e18, - borrowert0Np: 1_751.682692307692308500 * 1e18, - borrowerCollateralization: 1 * 1e18 - } + _assertAuction( + AuctionParams({ + borrower: _borrower, + active: false, + kicker: address(0), + bondSize: 0, + bondFactor: 0, + kickTime: 0, + kickMomp: 0, + totalBondEscrowed: 0, + auctionPrice: 0, + debtInAuction: 0, + thresholdPrice: 0, + neutralPrice: 0 + }) ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 0, + borrowerCollateral: 0, + borrowert0Np: 2_627.524038461538462750 * 1e18, + borrowerCollateralization: 1 * 1e18 + }); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 0, + borrowerCollateral: 1 * 1e18, + borrowert0Np: 1_751.682692307692308500 * 1e18, + borrowerCollateralization: 1 * 1e18 + }); // assert bucket used for settle - _assertBucket( - { - index: MAX_FENWICK_INDEX, - lpBalance: 0.000000137345389190751978670 * 1e27, - collateral: 1.375706158271934624 * 1e18, - deposit: 0, - exchangeRate: 0.999999999999999999994537736 * 1e27 - } - ); + _assertBucket({ + index: MAX_FENWICK_INDEX, + lpBalance: 0.000000137345389190 * 1e18, + collateral: 1.375706158271934624 * 1e18, + deposit: 0, + exchangeRate: 1.000000000007280914 * 1e18 + }); + assertEq(_quote.balanceOf(address(_pool)), 6_000 * 1e18); assertEq(_quote.balanceOf(_lender), 104_000 * 1e18); assertEq(_quote.balanceOf(_borrower), 5_100 * 1e18); assertEq(_quote.balanceOf(_borrower2), 13_000 * 1e18); // lender adds liquidity in min bucket and merge / removes 2 NFTs - _addLiquidity( - { - from: _lender, - amount: 100 * 1e18, - index: MAX_FENWICK_INDEX, - lpAward: 100.000000000000000000546226400 * 1e27, - newLup: MAX_PRICE - } - ); + _addLiquidity({ + from: _lender, + amount: 100 * 1e18, + index: MAX_FENWICK_INDEX, + lpAward: 99.999999999271908600 * 1e18, + newLup: MAX_PRICE + }); + uint256[] memory removalIndexes = new uint256[](3); removalIndexes[0] = 2500; removalIndexes[1] = 2501; removalIndexes[2] = MAX_FENWICK_INDEX; - _mergeOrRemoveCollateral( - { - from: _lender, - toIndex: MAX_FENWICK_INDEX, - noOfNFTsToRemove: 2, - collateralMerged: 2 * 1e18, - removeCollateralAtIndex: removalIndexes, - toIndexLps: 0 - } - ); + _mergeOrRemoveCollateral({ + from: _lender, + toIndex: MAX_FENWICK_INDEX, + noOfNFTsToRemove: 2, + collateralMerged: 2 * 1e18, + removeCollateralAtIndex: removalIndexes, + toIndexLps: 0 + }); + // lender merge and claim one more NFT removalIndexes = new uint256[](2); removalIndexes[0] = 2500; removalIndexes[1] = MAX_FENWICK_INDEX; - _mergeOrRemoveCollateral( - { - from: _lender, - toIndex: MAX_FENWICK_INDEX, - noOfNFTsToRemove: 1, - collateralMerged: 1 * 1e18, - removeCollateralAtIndex: removalIndexes, - toIndexLps: 0 - } - ); + _mergeOrRemoveCollateral({ + from: _lender, + toIndex: MAX_FENWICK_INDEX, + noOfNFTsToRemove: 1, + collateralMerged: 1 * 1e18, + removeCollateralAtIndex: removalIndexes, + toIndexLps: 0 + }); + // lender claims one more settled NFT _pool.removeCollateral(1, MAX_FENWICK_INDEX); @@ -381,15 +369,13 @@ contract ERC721PoolLiquidationsSettleTest is ERC721HelperContract { // check borrower 2 owner of 1 NFT assertEq(_collateral.ownerOf(51), _borrower2); - _assertBucket( - { - index: 2500, - lpBalance: 1_861.033884081553472671582113012 * 1e27, - collateral: 0, - deposit: 1861.636634299022017158 * 1e18, - exchangeRate: 1.000323879227898104734699503 * 1e27 - } - ); + _assertBucket({ + index: 2500, + lpBalance: 1_861.033884081553472950 * 1e18, + collateral: 0, + deposit: 1_861.636634299022017158 * 1e18, + exchangeRate: 1.000323879227898105 * 1e18 + }); } function testKickAndSettleSubsetPoolByRepay() external tearDown { @@ -397,16 +383,16 @@ contract ERC721PoolLiquidationsSettleTest is ERC721HelperContract { assertEq(_collateral.ownerOf(51), address(_pool)); assertEq(_collateral.ownerOf(53), address(_pool)); assertEq(_collateral.ownerOf(73), address(_pool)); + // borrower 2 repays debt and settles auction - _repayDebtNoLupCheck( - { - from: _borrower2, - borrower: _borrower2, - amountToRepay: 6_000 * 1e18, - amountRepaid: 0, - collateralToPull: 3 - } - ); + _repayDebtNoLupCheck({ + from: _borrower2, + borrower: _borrower2, + amountToRepay: 6_000 * 1e18, + amountRepaid: 0, + collateralToPull: 3 + }); + _assertAuction( AuctionParams({ borrower: _borrower2, @@ -423,34 +409,32 @@ contract ERC721PoolLiquidationsSettleTest is ERC721HelperContract { neutralPrice: 0 }) ); - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 0, - borrowerCollateral: 0, - borrowert0Np: 0, - borrowerCollateralization: 1 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 0, + borrowerCollateral: 0, + borrowert0Np: 0, + borrowerCollateralization: 1 * 1e18 + }); + // after settle: NFTs pledged by auctioned borrower are owned by the borrower assertEq(_collateral.ownerOf(51), address(_borrower2)); assertEq(_collateral.ownerOf(53), address(_borrower2)); assertEq(_collateral.ownerOf(73), address(_borrower2)); - // before auction settle: NFTs pledged by auctioned borrower are owned by the pool assertEq(_collateral.ownerOf(1), address(_pool)); assertEq(_collateral.ownerOf(3), address(_pool)); + // borrower repays debt and settles auction - _repayDebtNoLupCheck( - { - from: _borrower, - borrower: _borrower, - amountToRepay: 6_000 * 1e18, - amountRepaid: 0, - collateralToPull: 2 - } - ); + _repayDebtNoLupCheck({ + from: _borrower, + borrower: _borrower, + amountToRepay: 6_000 * 1e18, + amountRepaid: 0, + collateralToPull: 2 + }); + _assertAuction( AuctionParams({ borrower: _borrower, @@ -467,15 +451,14 @@ contract ERC721PoolLiquidationsSettleTest is ERC721HelperContract { neutralPrice: 0 }) ); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 0, - borrowerCollateral: 0, - borrowert0Np: 0, - borrowerCollateralization: 1 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 0, + borrowerCollateral: 0, + borrowert0Np: 0, + borrowerCollateralization: 1 * 1e18 + }); + // after settle: NFTs pledged by auctioned borrower are owned by the borrower assertEq(_collateral.ownerOf(1), address(_borrower)); assertEq(_collateral.ownerOf(3), address(_borrower)); diff --git a/tests/forge/ERC721Pool/ERC721PoolLiquidationsSettleAuction.t.sol b/tests/forge/ERC721Pool/ERC721PoolLiquidationsSettleAuction.t.sol new file mode 100644 index 000000000..24f55ccc2 --- /dev/null +++ b/tests/forge/ERC721Pool/ERC721PoolLiquidationsSettleAuction.t.sol @@ -0,0 +1,1288 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.14; + +import { ERC721Pool } from 'src/ERC721Pool.sol'; + +import { ERC721HelperContract } from "./ERC721DSTestPlus.sol"; + +import 'src/libraries/helpers/PoolHelper.sol'; + +contract ERC721PoolLiquidationsSettleAuctionTest is ERC721HelperContract { + + address internal _borrower; + address internal _borrower2; + address internal _lender; + + function setUp() external { + _borrower = makeAddr("borrower"); + _borrower2 = makeAddr("borrower2"); + _lender = makeAddr("lender"); + + // deploy subset pool + uint256[] memory subsetTokenIds = new uint256[](9); + subsetTokenIds[0] = 1; + subsetTokenIds[1] = 2; + subsetTokenIds[2] = 3; + subsetTokenIds[3] = 4; + subsetTokenIds[4] = 5; + subsetTokenIds[5] = 6; + subsetTokenIds[6] = 51; + subsetTokenIds[7] = 53; + subsetTokenIds[8] = 73; + _pool = _deploySubsetPool(subsetTokenIds); + + _mintAndApproveQuoteTokens(_lender, 120_000 * 1e18); + _mintAndApproveQuoteTokens(_borrower, 100 * 1e18); + _mintAndApproveQuoteTokens(_borrower2, 8_000 * 1e18); + + _mintAndApproveCollateralTokens(_borrower, 6); + _mintAndApproveCollateralTokens(_borrower2, 74); + + // Lender adds Quote token in one bucket + _addInitialLiquidity({ + from: _lender, + amount: 8_000 * 1e18, + index: 2500 + }); + _addInitialLiquidity({ + from: _lender, + amount: 2_000 * 1e18, + index: 2501 + }); + _addInitialLiquidity({ + from: _lender, + amount: 2_000 * 1e18, + index: 2502 + }); + _addInitialLiquidity({ + from: _lender, + amount: 1_000 * 1e18, + index: 2503 + }); + + // first borrower adds collateral token and borrows + uint256[] memory tokenIdsToAdd = new uint256[](2); + tokenIdsToAdd[0] = 1; + tokenIdsToAdd[1] = 3; + // borrower deposits two NFTs into the subset pool and borrows + _drawDebtNoLupCheck({ + from: _borrower, + borrower: _borrower, + amountToBorrow: 5_000 * 1e18, + limitIndex: 5000, + tokenIds: tokenIdsToAdd + }); + + // second borrower deposits three NFTs into the subset pool and borrows + tokenIdsToAdd = new uint256[](3); + tokenIdsToAdd[0] = 51; + tokenIdsToAdd[1] = 53; + tokenIdsToAdd[2] = 73; + // borrower deposits two NFTs into the subset pool and borrows + _drawDebtNoLupCheck({ + from: _borrower2, + borrower: _borrower2, + amountToBorrow: 5_000 * 1e18, + limitIndex: 5000, + tokenIds: tokenIdsToAdd + }); + + // kick both loans + _kickWithDeposit({ + from: _lender, + index: 2500, + borrower: _borrower, + debt: 5_067.367788461538463875 * 1e18, + collateral: 2 * 1e18, + bond: 1_501.442307692307693000 * 1e18, + removedFromDeposit: 1_501.442307692307693000 * 1e18, + transferAmount: 0, + lup: 3_825.305679430983794766 * 1e18 + }); + _kickWithDeposit({ + from: _lender, + index: 2500, + borrower: _borrower2, + debt: 5_067.367788461538463875 * 1e18, + collateral: 3 * 1e18, + bond: 1_501.442307692307693000 * 1e18, + removedFromDeposit: 1_501.442307692307693000 * 1e18, + transferAmount: 0, + lup: 99836282890 + }); + } + + function testSettlePartialDebtSubsetPool() external tearDown { + // the 2 token ids are owned by borrower before settle + assertEq(ERC721Pool(address(_pool)).borrowerTokenIds(_borrower, 0), 1); + assertEq(ERC721Pool(address(_pool)).borrowerTokenIds(_borrower, 1), 3); + + // skip to make loans clearable + skip(80 hours); + + _assertBucket({ + index: 2500, + lpBalance: 4_997.115384615384614 * 1e18, + collateral: 0, + deposit: 4_997.115384615384614 * 1e18, + exchangeRate: 1 * 1e18 + }); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 5_069.682183392068152308 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 2_627.524038461538462750 * 1e18, + borrowerCollateralization: 0.000000000039385618 * 1e18 + }); + + // first settle call settles partial borrower debt + _settle({ + from: _lender, + borrower: _borrower, + maxDepth: 1, + settledDebt: 4_997.146788514113366576 * 1e18 + }); + + // collateral in bucket used to settle auction increased with the amount used to settle debt + _assertBucket({ + index: 2500, + lpBalance: 4_997.115384615384614 * 1e18, + collateral: 1.293963857643160539 * 1e18, + deposit: 0, + exchangeRate: 1.000463012547417693 * 1e18 + }); + // partial borrower debt is settled, borrower collateral decreased with the amount used to settle debt + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 70.253071652712624346 * 1e18, + borrowerCollateral: 0.706036142356839461 * 1e18, + borrowert0Np: 2_627.524038461538462750 * 1e18, + borrowerCollateralization: 0.000000001003344372 * 1e18 + }); + + _assertCollateralInvariants(); + + // the 2 token ids are rebalanced and transferred to pool claimable tokens array after settle + assertEq(ERC721Pool(address(_pool)).bucketTokenIds(0), 3); + assertEq(ERC721Pool(address(_pool)).bucketTokenIds(1), 1); + + // all NFTs are owned by the pool + assertEq(_collateral.ownerOf(1), address(_pool)); + assertEq(_collateral.ownerOf(3), address(_pool)); + assertEq(_collateral.ownerOf(51), address(_pool)); + assertEq(_collateral.ownerOf(53), address(_pool)); + assertEq(_collateral.ownerOf(73), address(_pool)); + + // adding more liquidity to settle all auctions + _addLiquidityNoEventCheck({ + from: _lender, + amount: 20_000 * 1e18, + index: 2500 + }); + + _assertBucket({ + index: 2500, + lpBalance: 24_987.859419295112503013 * 1e18, + collateral: 1.293963857643160539 * 1e18, + deposit: 20_000 * 1e18, + exchangeRate: 1.000463012547417693 * 1e18 + }); + + _settle({ + from: _lender, + borrower: _borrower, + maxDepth: 1, + settledDebt: 70.220999947425097299 * 1e18 + }); + + + _assertBucket({ + index: 2500, + lpBalance: 24_987.859419295112503013 * 1e18, + collateral: 1.312146920864032689 * 1e18, + deposit: 19_929.746928347287375654 * 1e18, + exchangeRate: 1.000463012547417693 * 1e18 + }); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 0, + borrowerCollateral: 0, + borrowert0Np: 2_627.524038461538462750 * 1e18, + borrowerCollateralization: 1 * 1e18 + }); + + _assertCollateralInvariants(); + + _settle({ + from: _lender, + borrower: _borrower2, + maxDepth: 1, + settledDebt: 5_067.367788461538463875 * 1e18 + }); + + _assertBucket({ + index: 2500, + lpBalance: 24_987.859419295112503013 * 1e18, + collateral: 2.624293841728065377 * 1e18, + deposit: 14_860.064744955219223346 * 1e18, + exchangeRate: 1.000463012547417693 * 1e18 + }); + _assertBucket({ + index: 7388, + lpBalance: 0.000000137345389190 * 1e18, + collateral: 1.375706158271934623 * 1e18, + deposit: 0, + exchangeRate: 1.000000000007280914 * 1e18 + }); + // borrower 2 can claim 1 NFT token (id 51) after settle + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 0, + borrowerCollateral: 1 * 1e18, + borrowert0Np: 1_769.243311298076895206 * 1e18, + borrowerCollateralization: 1 * 1e18 + }); + + _assertCollateralInvariants(); + + assertEq(ERC721Pool(address(_pool)).borrowerTokenIds(_borrower2, 0), 51); + + // tokens used to settle auction are moved to pool claimable array + assertEq(ERC721Pool(address(_pool)).bucketTokenIds(0), 3); + assertEq(ERC721Pool(address(_pool)).bucketTokenIds(1), 1); + assertEq(ERC721Pool(address(_pool)).bucketTokenIds(2), 73); + assertEq(ERC721Pool(address(_pool)).bucketTokenIds(3), 53); + + // lender can claim 2 NFTs from bucket 2500 + changePrank(_lender); + _pool.removeCollateral(2, 2500); + + // lender adds liquidity in min bucket and merge / removes the other 2 NFTs + _addLiquidity({ + from: _lender, + amount: 100 * 1e18, + index: MAX_FENWICK_INDEX, + lpAward: 99.999999999271908600 * 1e18, + newLup: MAX_PRICE + }); + + uint256[] memory removalIndexes = new uint256[](2); + removalIndexes[0] = 2500; + removalIndexes[1] = MAX_FENWICK_INDEX; + _mergeOrRemoveCollateral({ + from: _lender, + toIndex: MAX_FENWICK_INDEX, + noOfNFTsToRemove: 2, + collateralMerged: 2 * 1e18, + removeCollateralAtIndex: removalIndexes, + toIndexLps: 0 + }); + + // the 4 NFTs claimed from pool are owned by lender + assertEq(_collateral.ownerOf(1), _lender); + assertEq(_collateral.ownerOf(3), _lender); + assertEq(_collateral.ownerOf(53), _lender); + assertEq(_collateral.ownerOf(73), _lender); + assertEq(_collateral.ownerOf(51), address(_pool)); + + // borrower 2 can pull 1 NFT from pool + _repayDebtNoLupCheck({ + from: _borrower2, + borrower: _borrower2, + amountToRepay: 0, + amountRepaid: 0, + collateralToPull: 1 + }); + + assertEq(_collateral.ownerOf(1), _lender); + assertEq(_collateral.ownerOf(3), _lender); + assertEq(_collateral.ownerOf(53), _lender); + assertEq(_collateral.ownerOf(73), _lender); + // the NFT pulled from pool is owned by lender + assertEq(_collateral.ownerOf(51), _borrower2); + + _assertBucket({ + index: 2500, + lpBalance: 14_853.187532758404035619 * 1e18, + collateral: 0, + deposit: 14_860.064744955219223346 * 1e18, + exchangeRate: 1.000463012547417693 * 1e18 + }); + _assertBucket({ + index: MAX_FENWICK_INDEX, + lpBalance: 99.999999999271908600 * 1e18, + collateral: 0, + deposit: 100 * 1e18, + exchangeRate: 1.000000000007280914 * 1e18 + }); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 0, + borrowerCollateral: 0, + borrowert0Np: 2_627.524038461538462750 * 1e18, + borrowerCollateralization: 1 * 1e18 + }); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 0, + borrowerCollateral: 0, + borrowert0Np: 0, + borrowerCollateralization: 1 * 1e18 + }); + + _assertCollateralInvariants(); + } + + function testDepositTakeAndSettleSubsetPool() external tearDown { + + // the 2 token ids are owned by borrower before settle + assertEq(ERC721Pool(address(_pool)).borrowerTokenIds(_borrower, 0), 1); + assertEq(ERC721Pool(address(_pool)).borrowerTokenIds(_borrower, 1), 3); + + _assertBucket({ + index: 2500, + lpBalance: 4997.115384615384614 * 1e18, + collateral: 0, + deposit: 4_997.115384615384614 * 1e18, + exchangeRate: 1 * 1e18 + }); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 5_067.367788461538463875 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 2_627.524038461538462750 * 1e18, + borrowerCollateralization: 0.000000000039403606 * 1e18 + }); + + skip(32 hours); + + _depositTake({ + from: _lender, + borrower: _borrower, + index: 2500 + }); + + _assertBucket({ + index: 2500, + lpBalance: 7_138.736263736263734674 * 1e18, + collateral: 1.848006454703595366 * 1e18, + deposit: 0, + exchangeRate: 1.000185179648802797 * 1e18 + }); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 425.033210305552211086 * 1e18, + borrowerCollateral: 0.151993545296404634 * 1e18, + borrowert0Np: 2_627.524038461538462750 * 1e18, + borrowerCollateralization: 0.000000000035701847 * 1e18 + }); + + _assertCollateralInvariants(); + + // borrower tries to repay remaining debt + _repayDebtNoLupCheck({ + from: _borrower2, + borrower: _borrower2, + amountToRepay: 1000 * 1e18, + amountRepaid: 0, + collateralToPull: 0 + }); + + skip(80 hours); + + _settle({ + from: _lender, + borrower: _borrower, + maxDepth: 1, + settledDebt: 424.955585758182281304 * 1e18 + }); + + _assertBucket({ + index: 2500, + lpBalance: 7_138.736263736263734674 * 1e18, + collateral: 1.848006454703595366 * 1e18, + deposit: 0, + exchangeRate: 1.000185179648802797 * 1e18 + }); + _assertBucket({ + index: 2501, + lpBalance: 2_000 * 1e18, + collateral: 0.110613668792155518 * 1e18, + deposit: 1_575.963420924493188203 * 1e18, + exchangeRate: 1.000605085927545054 * 1e18 + }); + _assertBucket({ + index: MAX_FENWICK_INDEX, + lpBalance: 4131213057, + collateral: 0.041379876504249116 * 1e18, + deposit: 0, + exchangeRate: 1 * 1e18 + }); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 0, + borrowerCollateral: 0, + borrowert0Np: 2_627.524038461538462750 * 1e18, + borrowerCollateralization: 1 * 1e18 + }); + + _assertCollateralInvariants(); + + // tokens used to settle auction are moved to pool claimable array + assertEq(ERC721Pool(address(_pool)).bucketTokenIds(0), 3); + assertEq(ERC721Pool(address(_pool)).bucketTokenIds(1), 1); + + // lender adds liquidity in min bucket and merge / removes the other 2 NFTs + _addLiquidity({ + from: _lender, + amount: 100 * 1e18, + index: MAX_FENWICK_INDEX, + lpAward: 100 * 1e18, + newLup: 3_806.274307891526195092 * 1e18 + }); + + uint256[] memory removalIndexes = new uint256[](3); + removalIndexes[0] = 2500; + removalIndexes[1] = 2501; + removalIndexes[2] = MAX_FENWICK_INDEX; + _mergeOrRemoveCollateral({ + from: _lender, + toIndex: MAX_FENWICK_INDEX, + noOfNFTsToRemove: 2, + collateralMerged: 2 * 1e18, + removeCollateralAtIndex: removalIndexes, + toIndexLps: 0 + }); + + // the 2 NFTs claimed from pool are owned by lender + assertEq(_collateral.ownerOf(1), _lender); + assertEq(_collateral.ownerOf(3), _lender); + + _assertCollateralInvariants(); + } + + function testDepositTakeAndSettleByPledgeSubsetPool() external tearDown { + + // the 2 token ids are owned by borrower before settle + assertEq(ERC721Pool(address(_pool)).borrowerTokenIds(_borrower, 0), 1); + assertEq(ERC721Pool(address(_pool)).borrowerTokenIds(_borrower, 1), 3); + + _assertBucket({ + index: 2500, + lpBalance: 4_997.115384615384614000 * 1e18, + collateral: 0, + deposit: 4_997.115384615384614000 * 1e18, + exchangeRate: 1 * 1e18 + }); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 5_067.367788461538463875 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 2_627.524038461538462750 * 1e18, + borrowerCollateralization: 0.000000000039403606 * 1e18 + }); + + skip(32 hours); + + _depositTake({ + from: _lender, + borrower: _borrower, + kicker: _lender, + index: 2500, + collateralArbed: 1.848006454703595366 * 1e18, + quoteTokenAmount: 7_140.058212410478208121 * 1e18, + bondChange: 2_142.017463723143462436 * 1e18, + isReward: true, + lpAwardTaker: 0, + lpAwardKicker: 2_141.620879120879120674 * 1e18 + }); + + _assertBucket({ + index: 2500, + lpBalance: 7_138.736263736263734674 * 1e18, + collateral: 1.848006454703595366 * 1e18, + deposit: 0, + exchangeRate: 1.000185179648802797 * 1e18 + }); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 425.033210305552211086 * 1e18, + borrowerCollateral: 0.151993545296404634 * 1e18, + borrowert0Np: 2_627.524038461538462750 * 1e18, + borrowerCollateralization: 0.000000000035701847 * 1e18 + }); + + _assertCollateralInvariants(); + + // borrower 2 repays entire debt and pulls collateral + _repayDebt({ + from: _borrower2, + borrower: _borrower2, + amountToRepay: 6_000 * 1e18, + amountRepaid: 5_068.293419619520519499 * 1e18, + collateralToPull: 3, + newLup: 3_844.432207828138682757 * 1e18 + }); + + // borrower exits from auction by pledging more collateral + uint256[] memory tokenIdsToAdd = new uint256[](3); + tokenIdsToAdd[0] = 2; + tokenIdsToAdd[1] = 4; + tokenIdsToAdd[2] = 5; + _drawDebtNoLupCheck({ + from: _borrower, + borrower: _borrower, + amountToBorrow: 0, + limitIndex: 0, + tokenIds: tokenIdsToAdd + }); + + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 425.033210305552211086 * 1e18, + borrowerCollateral: 3 * 1e18, + borrowert0Np: 149.442714324960768925 * 1e18, + borrowerCollateralization: 27.135048141751657646 * 1e18 + }); + _assertAuction( + AuctionParams({ + borrower: _borrower, + active: false, + kicker: address(0), + bondSize: 0, + bondFactor: 0, + kickTime: 0, + kickMomp: 0, + totalBondEscrowed: 0, + auctionPrice: 0, + debtInAuction: 0, + thresholdPrice: 141.677736768517403695 * 1e18, + neutralPrice: 0 + }) + ); + + _assertCollateralInvariants(); + + // the 3 new token ids pledged are owned by borrower + assertEq(ERC721Pool(address(_pool)).borrowerTokenIds(_borrower, 0), 1); + assertEq(ERC721Pool(address(_pool)).borrowerTokenIds(_borrower, 1), 3); + assertEq(ERC721Pool(address(_pool)).borrowerTokenIds(_borrower, 2), 2); + // tokens used to settle auction are moved to pool claimable array + assertEq(ERC721Pool(address(_pool)).bucketTokenIds(0), 5); + assertEq(ERC721Pool(address(_pool)).bucketTokenIds(1), 4); + + _assertBucket({ + index: 2500, + lpBalance: 7_138.736263736263734674 * 1e18, + collateral: 1.848006454703595366 * 1e18, + deposit: 0, + exchangeRate: 1.000185179648802797 * 1e18 + }); + _assertBucket({ + index: 6113, + lpBalance: 0.000008766823996015 * 1e18, + collateral: 0.151993545296404634 * 1e18, + deposit: 0, + exchangeRate: 1 * 1e18 + }); + + // lender adds liquidity in bucket 6113 and merge / removes the other 2 NFTs + _addLiquidity({ + from: _lender, + amount: 1000 * 1e18, + index: 6113, + lpAward: 1_000 * 1e18, + newLup: 3_844.432207828138682757 * 1e18 + }); + uint256[] memory removalIndexes = new uint256[](2); + removalIndexes[0] = 2500; + removalIndexes[1] = 6113; + _mergeOrRemoveCollateral({ + from: _lender, + toIndex: 6113, + noOfNFTsToRemove: 2, + collateralMerged: 2 * 1e18, + removeCollateralAtIndex: removalIndexes, + toIndexLps: 0 + }); + + // borrower repays entire debt and pulls collateral + _repayDebt({ + from: _borrower, + borrower: _borrower, + amountToRepay: 500 * 1e18, + amountRepaid: 425.033210305552211086 * 1e18, + collateralToPull: 3, + newLup: MAX_PRICE + }); + // borrower removes tokens from auction price bucket for compensated collateral fraction + _removeAllLiquidity({ + from: _borrower, + amount: 0.000008757551393712 * 1e18, + index: 6113, + newLup: MAX_PRICE, + lpRedeem: 0.000008766823996015 * 1e18 + }); + + // the 3 NFTs pulled from pool are owned by borrower + assertEq(_collateral.ownerOf(1), _borrower); + assertEq(_collateral.ownerOf(2), _borrower); + assertEq(_collateral.ownerOf(3), _borrower); + // the 2 NFTs claimed from pool are owned by lender + assertEq(_collateral.ownerOf(5), _lender); + assertEq(_collateral.ownerOf(4), _lender); + + _assertCollateralInvariants(); + } + + function testDepositTakeAndSettleByRepaySubsetPool() external tearDown { + + // the 2 token ids are owned by borrower before settle + assertEq(ERC721Pool(address(_pool)).borrowerTokenIds(_borrower, 0), 1); + assertEq(ERC721Pool(address(_pool)).borrowerTokenIds(_borrower, 1), 3); + + _assertBucket({ + index: 2500, + lpBalance: 4997.115384615384614 * 1e18, + collateral: 0, + deposit: 4_997.115384615384614000 * 1e18, + exchangeRate: 1 * 1e18 + }); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 5_067.367788461538463875 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 2_627.524038461538462750 * 1e18, + borrowerCollateralization: 0.000000000039403606 * 1e18 + }); + + skip(32 hours); + + _depositTake({ + from: _lender, + borrower: _borrower, + kicker: _lender, + index: 2500, + collateralArbed: 1.848006454703595366 * 1e18, + quoteTokenAmount: 7_140.058212410478208121 * 1e18, + bondChange: 2_142.017463723143462436 * 1e18, + isReward: true, + lpAwardTaker: 0, + lpAwardKicker: 2_141.620879120879120674 * 1e18 + }); + + _assertBucket({ + index: 2500, + lpBalance: 7_138.736263736263734674 * 1e18, + collateral: 1.848006454703595366 * 1e18, + deposit: 0, + exchangeRate: 1.000185179648802797 * 1e18 + }); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 425.033210305552211086 * 1e18, + borrowerCollateral: 0.151993545296404634 * 1e18, + borrowert0Np: 2_627.524038461538462750 * 1e18, + borrowerCollateralization: 0.000000000035701847 * 1e18 + }); + + _assertCollateralInvariants(); + + // borrower 2 repays entire debt and pulls collateral + _repayDebt({ + from: _borrower2, + borrower: _borrower2, + amountToRepay: 6_000 * 1e18, + amountRepaid: 5_068.293419619520519499 * 1e18, + collateralToPull: 3, + newLup: 3_844.432207828138682757 * 1e18 + }); + // borrower exits from auction by repaying the debt + _repayDebt({ + from: _borrower, + borrower: _borrower, + amountToRepay: 500 * 1e18, + amountRepaid: 425.033210305552211086 * 1e18, + collateralToPull: 0, + newLup: MAX_PRICE + }); + + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 0, + borrowerCollateral: 0, + borrowert0Np: 0, + borrowerCollateralization: 1 * 1e18 + }); + _assertAuction( + AuctionParams({ + borrower: _borrower, + active: false, + kicker: address(0), + bondSize: 0, + bondFactor: 0, + kickTime: 0, + kickMomp: 0, + totalBondEscrowed: 0, + auctionPrice: 0, + debtInAuction: 0, + thresholdPrice: 0, + neutralPrice: 0 + }) + ); + + _assertCollateralInvariants(); + + // tokens used to settle auction are moved to pool claimable array + assertEq(ERC721Pool(address(_pool)).bucketTokenIds(0), 3); + assertEq(ERC721Pool(address(_pool)).bucketTokenIds(1), 1); + + _assertBucket({ + index: 2500, + lpBalance: 7_138.736263736263734674 * 1e18, + collateral: 1.848006454703595366 * 1e18, + deposit: 0, + exchangeRate: 1.000185179648802797 * 1e18 + }); + _assertBucket({ + index: 6113, + lpBalance: 0.000008766823996015 * 1e18, + collateral: 0.151993545296404634 * 1e18, + deposit: 0, + exchangeRate: 1 * 1e18 + }); + + // lender adds liquidity in bucket 6113 and merge / removes the other 2 NFTs + _addLiquidity({ + from: _lender, + amount: 1000 * 1e18, + index: 6113, + lpAward: 1_000 * 1e18, + newLup: MAX_PRICE + }); + uint256[] memory removalIndexes = new uint256[](2); + removalIndexes[0] = 2500; + removalIndexes[1] = 6113; + _mergeOrRemoveCollateral({ + from: _lender, + toIndex: 6113, + noOfNFTsToRemove: 2, + collateralMerged: 2 * 1e18, + removeCollateralAtIndex: removalIndexes, + toIndexLps: 0 + }); + + // the 2 NFTs claimed from pool are owned by lender + assertEq(_collateral.ownerOf(3), _lender); + assertEq(_collateral.ownerOf(1), _lender); + + _assertCollateralInvariants(); + + // borrower removes tokens from auction price bucket for compensated collateral fraction + _removeAllLiquidity({ + from: _borrower, + amount: 0.000008757551393712 * 1e18, + index: 6113, + newLup: MAX_PRICE, + lpRedeem: 0.000008766823996015 * 1e18 + }); + + } + + function testDepositTakeAndSettleByRegularTakeSubsetPool() external tearDown { + + // the 2 token ids are owned by borrower before settle + assertEq(ERC721Pool(address(_pool)).borrowerTokenIds(_borrower, 0), 1); + assertEq(ERC721Pool(address(_pool)).borrowerTokenIds(_borrower, 1), 3); + + _assertBucket({ + index: 2502, + lpBalance: 2_000 * 1e18, + collateral: 0, + deposit: 2_000 * 1e18, + exchangeRate: 1 * 1e18 + }); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 5_067.367788461538463875 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 2_627.524038461538462750 * 1e18, + borrowerCollateralization: 0.000000000039403606 * 1e18 + }); + + skip(4 hours); + + _addLiquidity({ + from: _lender, + amount: 1_000 * 1e18, + index: 2000, + lpAward: 1_000 * 1e18, + newLup: 3_806.274307891526195092 * 1e18 + }); + + _depositTake({ + from: _lender, + borrower: _borrower, + kicker: _lender, + index: 2000, + collateralArbed: 0.021378186081598093 * 1e18, + quoteTokenAmount: 999.9999999999999908 * 1e18, + bondChange: 299.99999999999999724 * 1e18, + isReward: false, + lpAwardTaker: 0, + lpAwardKicker: 0 + }); + + _assertBucket({ + index: 2000, + lpBalance: 1_000 * 1e18, + collateral: 0.021378186081598093 * 1e18, + deposit: 0, + exchangeRate: 0.999999999999999991 * 1e18 + }); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 4_422.207326928504959735 * 1e18, + borrowerCollateral: 1.978621813918401907 * 1e18, + borrowert0Np: 2_627.524038461538462750 * 1e18, + borrowerCollateralization: 1.703035796058482710 * 1e18 + }); + + _assertCollateralInvariants(); + + assertEq(_quote.balanceOf(_borrower), 5_100 * 1e18); + assertEq(_quote.balanceOf(address(_pool)), 4_000 * 1e18); + + // borrower exits from auction by regular take + _take({ + from: _lender, + borrower: _borrower, + maxCollateral: 1, + bondChange: 1_201.442307692307695760 * 1e18, + givenAmount: 4_422.207326928504959735 * 1e18, + collateralTaken: 0.286141493566424944 * 1e18, + isReward: false + }); + + assertEq(_quote.balanceOf(_borrower), 16_132.410148540612418451 * 1e18); + assertEq(_quote.balanceOf(address(_pool)), 8_422.207326928504959735 * 1e18); + + // borrower 2 repays entire debt and pulls collateral + _repayDebt({ + from: _borrower2, + borrower: _borrower2, + amountToRepay: 6_000 * 1e18, + amountRepaid: 5_067.483483110752298817 * 1e18, + collateralToPull: 3, + newLup: MAX_PRICE + }); + + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 0, + borrowerCollateral: 0, + borrowert0Np: 0, + borrowerCollateralization: 1 * 1e18 + }); + _assertAuction( + AuctionParams({ + borrower: _borrower, + active: false, + kicker: address(0), + bondSize: 0, + bondFactor: 0, + kickTime: 0, + kickMomp: 0, + totalBondEscrowed: 0, + auctionPrice: 0, + debtInAuction: 0, + thresholdPrice: 0, + neutralPrice: 0 + }) + ); + + _assertCollateralInvariants(); + + // remaining token is moved to pool claimable array + assertEq(ERC721Pool(address(_pool)).bucketTokenIds(0), 1); + + _assertBucket({ + index: 2000, + lpBalance: 1_000 * 1e18, + collateral: 0.021378186081598093 * 1e18, + deposit: 0, + exchangeRate: 0.999999999999999991 * 1e18 + }); + _assertBucket({ + index: 2222, + lpBalance: 15_127.888999922350308085 * 1e18, + collateral: 0.978621813918401907 * 1e18, + deposit: 0, + exchangeRate: 1 * 1e18 + }); + + // lender adds liquidity in bucket 2222 and merge / removes remaining NFTs + _addLiquidity({ + from: _lender, + amount: 20_000 * 1e18, + index: 2222, + lpAward: 20_000 * 1e18, + newLup: MAX_PRICE + }); + _addLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2000, + lpAward: 10_000.00000000000009 * 1e18, + newLup: MAX_PRICE + }); + uint256[] memory removalIndexes = new uint256[](2); + removalIndexes[0] = 2000; + removalIndexes[1] = 2222; + _mergeOrRemoveCollateral({ + from: _lender, + toIndex: 2222, + noOfNFTsToRemove: 1, + collateralMerged: 1 * 1e18, + removeCollateralAtIndex: removalIndexes, + toIndexLps: 0 + }); + + // the 2 NFTs (one taken, one claimed) are owned by lender + assertEq(_collateral.ownerOf(3), _lender); + assertEq(_collateral.ownerOf(1), _lender); + + _assertCollateralInvariants(); + + // borrower removes tokens from auction price bucket for compensated collateral fraction + _removeAllLiquidity({ + from: _borrower, + amount: 15_113.342952807040348884 * 1e18, + index: 2222, + newLup: MAX_PRICE, + lpRedeem: 15_127.888999922350308085 * 1e18 + }); + } + + function testDepositTakeAndSettleByBucketTakeSubsetPool() external tearDown { + + // the 2 token ids are owned by borrower before settle + assertEq(ERC721Pool(address(_pool)).borrowerTokenIds(_borrower, 0), 1); + assertEq(ERC721Pool(address(_pool)).borrowerTokenIds(_borrower, 1), 3); + + _assertBucket({ + index: 2502, + lpBalance: 2_000 * 1e18, + collateral: 0, + deposit: 2_000 * 1e18, + exchangeRate: 1 * 1e18 + }); + + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 5_067.367788461538463875 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 2_627.524038461538462750 * 1e18, + borrowerCollateralization: 0.000000000039403606 * 1e18 + }); + + skip(32 hours); + + _depositTake({ + from: _lender, + borrower: _borrower, + kicker: _lender, + index: 2502, + collateralArbed: 0.747044074730990508 * 1e18, + quoteTokenAmount: 2_857.671941853722277733 * 1e18, + bondChange: 857.301582556116683320 * 1e18, + isReward: true, + lpAwardTaker: 0, + lpAwardKicker: 857.142857142857143034 * 1e18 + }); + + _assertBucket({ + index: 2502, + lpBalance: 2_857.142857142857143034 * 1e18, + collateral: 0.747044074730990508 * 1e18, + deposit: 0, + exchangeRate: 1.000185179648802797 * 1e18 + }); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 3_422.703599695281361864 * 1e18, + borrowerCollateral: 1.252955925269009492 * 1e18, + borrowert0Np: 2_627.524038461538462750 * 1e18, + borrowerCollateralization: 0.000000000036547267 * 1e18 + }); + + _assertCollateralInvariants(); + + // borrower 2 repays entire debt and pulls collateral + _repayDebt({ + from: _borrower2, + borrower: _borrower2, + amountToRepay: 6_000 * 1e18, + amountRepaid: 5_068.293419619520519499 * 1e18, + collateralToPull: 3, + newLup: 3_863.654368867279344664 * 1e18 + }); + + // borrower exits from auction by bucket take: lender adds quote token at a higher priced bucket and calls deposit take + _addLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2400, + lpAward: 10_000 * 1e18, + newLup: 6_362.157913642177655049 * 1e18 + }); + + _depositTake({ + from: _lender, + borrower: _borrower, + kicker: _lender, + index: 2400, + collateralArbed: 0.768540585971418892 * 1e18, + quoteTokenAmount: 4_889.576570993259088377 * 1e18, + bondChange: 1_466.872971297977726513 * 1e18, + isReward: true, + lpAwardTaker: 0, + lpAwardKicker: 1_466.872971297977726513 * 1e18 + }); + + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 0, + borrowerCollateral: 0, + borrowert0Np: 0, + borrowerCollateralization: 1 * 1e18 + }); + _assertAuction( + AuctionParams({ + borrower: _borrower, + active: false, + kicker: address(0), + bondSize: 0, + bondFactor: 0, + kickTime: 0, + kickMomp: 0, + totalBondEscrowed: 0, + auctionPrice: 0, + debtInAuction: 0, + thresholdPrice: 0, + neutralPrice: 0 + }) + ); + + _assertCollateralInvariants(); + + // tokens used to settle auction are moved to pool claimable array + assertEq(ERC721Pool(address(_pool)).bucketTokenIds(0), 3); + assertEq(ERC721Pool(address(_pool)).bucketTokenIds(1), 1); + + _assertBucket({ + index: 2400, + lpBalance: 11_466.872971297977726513 * 1e18, + collateral: 0.768540585971418892 * 1e18, + deposit: 6_577.296400304718638136 * 1e18, + exchangeRate: 1 * 1e18 + }); + _assertBucket({ + index: 6113, + lpBalance: 0.000027940555056533 * 1e18, + collateral: 0.484415339297590600 * 1e18, + deposit: 0, + exchangeRate: 1 * 1e18 + }); + + // lender adds liquidity in bucket 6113 and merge / removes the other 2 NFTs + _addLiquidity({ + from: _lender, + amount: 1_000 * 1e18, + index: 6113, + lpAward: 1_000 * 1e18, + newLup: MAX_PRICE + }); + uint256[] memory removalIndexes = new uint256[](3); + removalIndexes[0] = 2400; + removalIndexes[1] = 2502; + removalIndexes[2] = 6113; + _mergeOrRemoveCollateral({ + from: _lender, + toIndex: 6113, + noOfNFTsToRemove: 2, + collateralMerged: 2 * 1e18, + removeCollateralAtIndex: removalIndexes, + toIndexLps: 0 + }); + + // the 2 NFTs claimed from pool are owned by lender + assertEq(_collateral.ownerOf(3), _lender); + assertEq(_collateral.ownerOf(1), _lender); + + _assertCollateralInvariants(); + + // borrower removes tokens from auction price bucket for compensated collateral fraction + _removeAllLiquidity({ + from: _borrower, + amount: 0.000027911002546377 * 1e18, + index: 6113, + newLup: MAX_PRICE, + lpRedeem: 0.000027940555056533 * 1e18 + }); + } + + function testDepositTakeAndSettleBySettleSubsetPool() external { + + // the 2 token ids are owned by borrower before settle + assertEq(ERC721Pool(address(_pool)).borrowerTokenIds(_borrower, 0), 1); + assertEq(ERC721Pool(address(_pool)).borrowerTokenIds(_borrower, 1), 3); + + _assertBucket({ + index: 2502, + lpBalance: 2_000 * 1e18, + collateral: 0, + deposit: 2_000 * 1e18, + exchangeRate: 1 * 1e18 + }); + + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 5_067.367788461538463875 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 2_627.524038461538462750 * 1e18, + borrowerCollateralization: 0.000000000039403606 * 1e18 + }); + + skip(32 hours); + + _depositTake({ + from: _lender, + borrower: _borrower, + kicker: _lender, + index: 2502, + collateralArbed: 0.747044074730990508 * 1e18, + quoteTokenAmount: 2_857.671941853722277733 * 1e18, + bondChange: 857.301582556116683320 * 1e18, + isReward: true, + lpAwardTaker: 0, + lpAwardKicker: 857.142857142857143034 * 1e18 + }); + + _assertBucket({ + index: 2502, + lpBalance: 2_857.142857142857143034 * 1e18, + collateral: 0.747044074730990508 * 1e18, + deposit: 0, + exchangeRate: 1.000185179648802797 * 1e18 + }); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 3_422.703599695281361864 * 1e18, + borrowerCollateral: 1.252955925269009492 * 1e18, + borrowert0Np: 2_627.524038461538462750 * 1e18, + borrowerCollateralization: 0.000000000036547267 * 1e18 + }); + + _assertCollateralInvariants(); + + // borrower 2 repays entire debt and pulls collateral + _repayDebt({ + from: _borrower2, + borrower: _borrower2, + amountToRepay: 6_000 * 1e18, + amountRepaid: 5_068.293419619520519499 * 1e18, + collateralToPull: 3, + newLup: 3_863.654368867279344664 * 1e18 + }); + + skip(72 hours); + + // borrower exits from auction by pool debt settle + _settle({ + from: _lender, + borrower: _borrower, + maxDepth: 10, + settledDebt: 3_422.078505440842334340 * 1e18 + }); + + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 0, + borrowerCollateral: 0, + borrowert0Np: 2_627.524038461538462750 * 1e18, + borrowerCollateralization: 1 * 1e18 + }); + _assertAuction( + AuctionParams({ + borrower: _borrower, + active: false, + kicker: address(0), + bondSize: 0, + bondFactor: 0, + kickTime: 0, + kickMomp: 0, + totalBondEscrowed: 0, + auctionPrice: 0, + debtInAuction: 0, + thresholdPrice: 0, + neutralPrice: 0 + }) + ); + + _assertCollateralInvariants(); + + // tokens used to settle auction are moved to pool claimable array + assertEq(ERC721Pool(address(_pool)).bucketTokenIds(0), 3); + assertEq(ERC721Pool(address(_pool)).bucketTokenIds(1), 1); + + _assertBucket({ + index: 2500, + lpBalance: 4_997.115384615384614000 * 1e18, + collateral: 0.886272650740532744 * 1e18, + deposit: 1_574.636172339194134488 * 1e18, + exchangeRate: 1.000354601931047794 * 1e18 + }); + _assertBucket({ + index: 2502, + lpBalance: 2_857.142857142857143034 * 1e18, + collateral: 0.747044074730990508 * 1e18, + deposit: 0, + exchangeRate: 1.000185179648802797 * 1e18 + }); + _assertBucket({ + index: 7388, + lpBalance: 0.000000036608295127 * 1e18, + collateral: 0.366683274528476748 * 1e18, + deposit: 0, + exchangeRate: 1 * 1e18 + }); + + // lender adds liquidity in bucket 7388 and merge / removes the other 2 NFTs + _addLiquidity({ + from: _lender, + amount: 1_000 * 1e18, + index: 7388, + lpAward: 1_000 * 1e18, + newLup: MAX_PRICE + }); + uint256[] memory removalIndexes = new uint256[](3); + removalIndexes[0] = 2500; + removalIndexes[1] = 2502; + removalIndexes[2] = 7388; + _mergeOrRemoveCollateral({ + from: _lender, + toIndex: 7388, + noOfNFTsToRemove: 2, + collateralMerged: 2 * 1e18, + removeCollateralAtIndex: removalIndexes, + toIndexLps: 0 + }); + + // the 2 NFTs claimed from pool are owned by lender + assertEq(_collateral.ownerOf(3), _lender); + assertEq(_collateral.ownerOf(1), _lender); + + _assertCollateralInvariants(); + } +} diff --git a/tests/forge/ERC721Pool/ERC721PoolLiquidationsTake.t.sol b/tests/forge/ERC721Pool/ERC721PoolLiquidationsTake.t.sol index 939607332..59531d696 100644 --- a/tests/forge/ERC721Pool/ERC721PoolLiquidationsTake.t.sol +++ b/tests/forge/ERC721Pool/ERC721PoolLiquidationsTake.t.sol @@ -3,6 +3,8 @@ pragma solidity 0.8.14; import { ERC721HelperContract } from "./ERC721DSTestPlus.sol"; +import { NFTNoopTakeExample } from "../interactions/NFTTakeExample.sol"; + import 'src/libraries/helpers/PoolHelper.sol'; contract ERC721PoolLiquidationsTakeTest is ERC721HelperContract { @@ -10,11 +12,13 @@ contract ERC721PoolLiquidationsTakeTest is ERC721HelperContract { address internal _borrower; address internal _borrower2; address internal _lender; + address internal _withdrawRecipient; function setUp() external { - _borrower = makeAddr("borrower"); - _borrower2 = makeAddr("borrower2"); - _lender = makeAddr("lender"); + _borrower = makeAddr("borrower"); + _borrower2 = makeAddr("borrower2"); + _lender = makeAddr("lender"); + _withdrawRecipient = makeAddr("withdrawRecipient"); // deploy subset pool uint256[] memory subsetTokenIds = new uint256[](6); @@ -34,84 +38,66 @@ contract ERC721PoolLiquidationsTakeTest is ERC721HelperContract { _mintAndApproveCollateralTokens(_borrower2, 74); // Lender adds Quote token accross 5 prices - _addInitialLiquidity( - { - from: _lender, - amount: 2_000 * 1e18, - index: _i9_91 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 5_000 * 1e18, - index: _i9_81 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 11_000 * 1e18, - index: _i9_72 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 25_000 * 1e18, - index: _i9_62 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 30_000 * 1e18, - index: _i9_52 - } - ); + _addInitialLiquidity({ + from: _lender, + amount: 2_000 * 1e18, + index: _i9_91 + }); + _addInitialLiquidity({ + from: _lender, + amount: 5_000 * 1e18, + index: _i9_81 + }); + _addInitialLiquidity({ + from: _lender, + amount: 11_000 * 1e18, + index: _i9_72 + }); + _addInitialLiquidity({ + from: _lender, + amount: 25_000 * 1e18, + index: _i9_62 + }); + _addInitialLiquidity({ + from: _lender, + amount: 30_000 * 1e18, + index: _i9_52 + }); - // first borrower adds collateral token and borrows + // first borrower adds collateral token and borrows uint256[] memory tokenIdsToAdd = new uint256[](2); tokenIdsToAdd[0] = 1; tokenIdsToAdd[1] = 3; // borrower deposits two NFTs into the subset pool and borrows - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - tokenIds: tokenIdsToAdd - } - ); - _borrow( - { - from: _borrower, - amount: 19.8 * 1e18, - indexLimit: _i9_91, - newLup: 9.917184843435912074 * 1e18 - } - ); + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + tokenIds: tokenIdsToAdd + }); + _borrow({ + from: _borrower, + amount: 19.8 * 1e18, + indexLimit: _i9_91, + newLup: 9.917184843435912074 * 1e18 + }); // second borrower deposits three NFTs into the subset pool and borrows tokenIdsToAdd = new uint256[](3); tokenIdsToAdd[0] = 51; tokenIdsToAdd[1] = 53; tokenIdsToAdd[2] = 73; - _pledgeCollateral( - { - from: _borrower2, - borrower: _borrower2, - tokenIds: tokenIdsToAdd - } - ); - _borrow( - { - from: _borrower2, - amount: 15 * 1e18, - indexLimit: _i9_72, - newLup: 9.917184843435912074 * 1e18 - } - ); + _pledgeCollateral({ + from: _borrower2, + borrower: _borrower2, + tokenIds: tokenIdsToAdd + }); + _borrow({ + from: _borrower2, + amount: 15 * 1e18, + indexLimit: _i9_72, + newLup: 9.917184843435912074 * 1e18 + }); /*****************************/ /*** Assert pre-kick state ***/ @@ -134,24 +120,21 @@ contract ERC721PoolLiquidationsTakeTest is ERC721HelperContract { interestRateUpdate: _startTime }) ); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 19.819038461538461548 * 1e18, - borrowerCollateral: 2 * 1e18, - borrowert0Np: 10.404995192307692312 * 1e18, - borrowerCollateralization: 1.000773560501591181 * 1e18 - } - ); - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 15.014423076923076930 * 1e18, - borrowerCollateral: 3 * 1e18, - borrowert0Np: 5.255048076923076925 * 1e18, - borrowerCollateralization: 1.981531649793150539 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 19.819038461538461548 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 10.404995192307692312 * 1e18, + borrowerCollateralization: 1.000773560501591181 * 1e18 + }); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 15.014423076923076930 * 1e18, + borrowerCollateral: 3 * 1e18, + borrowert0Np: 5.255048076923076925 * 1e18, + borrowerCollateralization: 1.981531649793150539 * 1e18 + }); + assertEq(_quote.balanceOf(_lender), 47_000 * 1e18); } @@ -177,26 +160,22 @@ contract ERC721PoolLiquidationsTakeTest is ERC721HelperContract { }) ); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 22.728719829841718804 * 1e18, - borrowerCollateral: 2 * 1e18, - borrowert0Np: 10.404995192307692312 * 1e18, - borrowerCollateralization: 0.872656701977127996 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 22.728719829841718804 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 10.404995192307692312 * 1e18, + borrowerCollateralization: 0.872656701977127996 * 1e18 + }); - _kick( - { - from: _lender, - borrower: _borrower, - debt: 23.012828827714740289 * 1e18, - collateral: 2 * 1e18, - bond: 0.227287198298417188 * 1e18, - transferAmount: 0.227287198298417188 * 1e18 - } - ); + _kick({ + from: _lender, + borrower: _borrower, + debt: 23.012828827714740289 * 1e18, + collateral: 2 * 1e18, + bond: 0.227287198298417188 * 1e18, + transferAmount: 0.227287198298417188 * 1e18 + }); /******************************/ /*** Assert Post-kick state ***/ @@ -219,25 +198,23 @@ contract ERC721PoolLiquidationsTakeTest is ERC721HelperContract { interestRateUpdate: block.timestamp }) ); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 23.012828827714740289 * 1e18, - borrowerCollateral: 2 * 1e18, - borrowert0Np: 10.404995192307692312 * 1e18, - borrowerCollateralization: 0.861883162446546169 * 1e18 - } - ); - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 17.218727143819483942 * 1e18, - borrowerCollateral: 3 * 1e18, - borrowert0Np: 5.255048076923076925 * 1e18, - borrowerCollateralization: 1.727860269914713433 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 23.012828827714740289 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 10.404995192307692312 * 1e18, + borrowerCollateralization: 0.861883162446546169 * 1e18 + }); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 17.218727143819483942 * 1e18, + borrowerCollateral: 3 * 1e18, + borrowert0Np: 5.255048076923076925 * 1e18, + borrowerCollateralization: 1.727860269914713433 * 1e18 + }); + assertEq(_quote.balanceOf(_lender), 46_999.772712801701582812 * 1e18); + _assertAuction( AuctionParams({ borrower: _borrower, @@ -254,22 +231,22 @@ contract ERC721PoolLiquidationsTakeTest is ERC721HelperContract { neutralPrice: 11.932577910666902372 * 1e18 }) ); - _assertKicker( - { - kicker: _lender, - claimable: 0, - locked: 0.227287198298417188 * 1e18 - } - ); + _assertKicker({ + kicker: _lender, + claimable: 0, + locked: 0.227287198298417188 * 1e18 + }); skip(5.5 hours); // before take: NFTs pledged by auctioned borrower are owned by the pool assertEq(_collateral.ownerOf(3), address(_pool)); assertEq(_collateral.ownerOf(1), address(_pool)); + // before take: check quote token balances of taker and borrower assertEq(_quote.balanceOf(_lender), 46_999.772712801701582812 * 1e18); assertEq(_quote.balanceOf(_borrower), 119.8 * 1e18); + _assertAuction( AuctionParams({ borrower: _borrower, @@ -287,15 +264,13 @@ contract ERC721PoolLiquidationsTakeTest is ERC721HelperContract { }) ); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 23.013479028125331754 * 1e18, - borrowerCollateral: 2 * 1e18, - borrowert0Np: 10.404995192307692312 * 1e18, - borrowerCollateralization: 0.861858811639550854 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 23.013479028125331754 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 10.404995192307692312 * 1e18, + borrowerCollateralization: 0.861858811639550854 * 1e18 + }); uint256 snapshot = vm.snapshot(); @@ -303,17 +278,15 @@ contract ERC721PoolLiquidationsTakeTest is ERC721HelperContract { /* Take partial collateral tokens (1) ***/ /****************************************/ - _take( - { - from: _lender, - borrower: _borrower, - maxCollateral: 1, - bondChange: 0.168752135153387434 * 1e18, - givenAmount: 16.875213515338743424 * 1e18, - collateralTaken: 1.0 * 1e18, - isReward: false - } - ); + _take({ + from: _lender, + borrower: _borrower, + maxCollateral: 1, + bondChange: 0.168752135153387434 * 1e18, + givenAmount: 16.875213515338743424 * 1e18, + collateralTaken: 1.0 * 1e18, + isReward: false + }); _assertPool( PoolParams({ @@ -333,15 +306,13 @@ contract ERC721PoolLiquidationsTakeTest is ERC721HelperContract { }) ); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 7.749209044755361552 * 1e18, - borrowerCollateral: 1 * 1e18, - borrowert0Np: 7.061045370627448273 * 1e18, - borrowerCollateralization: 1.279767365438131935 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 7.749209044755361552 * 1e18, + borrowerCollateral: 1 * 1e18, + borrowert0Np: 7.061045370627448273 * 1e18, + borrowerCollateralization: 1.279767365438131935 * 1e18 + }); _assertAuction( AuctionParams({ @@ -359,18 +330,16 @@ contract ERC721PoolLiquidationsTakeTest is ERC721HelperContract { neutralPrice: 0 }) ); - - _assertKicker( - { - kicker: address(0), - claimable: 0, - locked: 0 - } - ); + _assertKicker({ + kicker: address(0), + claimable: 0, + locked: 0 + }); // after take: one NFT pledged by liquidated borrower is owned by the taker assertEq(_collateral.ownerOf(3), _lender); assertEq(_collateral.ownerOf(1), address(_pool)); + // after take: check quote token balances of taker and borrower assertEq(_quote.balanceOf(_lender), 46_982.897499286362839388 * 1e18); assertEq(_quote.balanceOf(_borrower), 119.8 * 1e18); // no additional tokens as there is no rounding of collateral taken (1) @@ -381,17 +350,15 @@ contract ERC721PoolLiquidationsTakeTest is ERC721HelperContract { /*** Take all collateral tokens (2) ***/ /**************************************/ - _take( - { - from: _lender, - borrower: _borrower, - maxCollateral: 2, - bondChange: 0.227287198298417188 * 1e18, - givenAmount: 24.624422560094104976 * 1e18, - collateralTaken: 1.459206577606363895 * 1e18, // not a rounded collateral, difference of 2 - 1.16 collateral should go to borrower in quote tokens at auction price - isReward: false - } - ); + _take({ + from: _lender, + borrower: _borrower, + maxCollateral: 2, + bondChange: 0.227287198298417188 * 1e18, + givenAmount: 24.624422560094104976 * 1e18, + collateralTaken: 1.459206577606363895 * 1e18, // not a rounded collateral, difference of 2 - 1.16 collateral should go to borrower in quote tokens at auction price + isReward: false + }); _assertPool( PoolParams({ @@ -411,15 +378,13 @@ contract ERC721PoolLiquidationsTakeTest is ERC721HelperContract { }) ); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 0, - borrowerCollateral: 0, - borrowert0Np: 0, - borrowerCollateralization: 1 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 0, + borrowerCollateral: 0, + borrowert0Np: 0, + borrowerCollateralization: 1 * 1e18 + }); _assertAuction( AuctionParams({ @@ -437,18 +402,16 @@ contract ERC721PoolLiquidationsTakeTest is ERC721HelperContract { neutralPrice: 0 }) ); - - _assertKicker( - { - kicker: address(0), - claimable: 0, - locked: 0 * 1e18 - } - ); + _assertKicker({ + kicker: address(0), + claimable: 0, + locked: 0 * 1e18 + }); // after take: NFTs pledged by liquidated borrower are owned by the taker assertEq(_collateral.ownerOf(3), _lender); assertEq(_collateral.ownerOf(1), _lender); + // after take: check quote token balances of taker and borrower assertEq(_quote.balanceOf(_lender), 46_966.022285771024095971 * 1e18); assertEq(_quote.balanceOf(_borrower), 128.926004470583381865 * 1e18); // borrower gets quote tokens from the difference of rounded collateral (2) and needed collateral (1.16) at auction price (19.8) = 16.6 additional tokens @@ -475,27 +438,22 @@ contract ERC721PoolLiquidationsTakeTest is ERC721HelperContract { neutralPrice: 0 }) ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 22.728719829841718804 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 10.404995192307692312 * 1e18, + borrowerCollateralization: 0.872656701977127996 * 1e18 + }); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 22.728719829841718804 * 1e18, - borrowerCollateral: 2 * 1e18, - borrowert0Np: 10.404995192307692312 * 1e18, - borrowerCollateralization: 0.872656701977127996 * 1e18 - } - ); - - _kick( - { - from: _lender, - borrower: _borrower, - debt: 23.012828827714740289 * 1e18, - collateral: 2 * 1e18, - bond: 0.227287198298417188 * 1e18, - transferAmount: 0.227287198298417188 * 1e18 - } - ); + _kick({ + from: _lender, + borrower: _borrower, + debt: 23.012828827714740289 * 1e18, + collateral: 2 * 1e18, + bond: 0.227287198298417188 * 1e18, + transferAmount: 0.227287198298417188 * 1e18 + }); /******************************/ /*** Assert Post-kick state ***/ @@ -518,25 +476,23 @@ contract ERC721PoolLiquidationsTakeTest is ERC721HelperContract { interestRateUpdate: block.timestamp }) ); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 23.012828827714740289 * 1e18, - borrowerCollateral: 2 * 1e18, - borrowert0Np: 10.404995192307692312 * 1e18, - borrowerCollateralization: 0.861883162446546169 * 1e18 - } - ); - _assertBorrower( - { - borrower: _borrower2, - borrowerDebt: 17.218727143819483942 * 1e18, - borrowerCollateral: 3 * 1e18, - borrowert0Np: 5.255048076923076925 * 1e18, - borrowerCollateralization: 1.727860269914713433 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 23.012828827714740289 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 10.404995192307692312 * 1e18, + borrowerCollateralization: 0.861883162446546169 * 1e18 + }); + _assertBorrower({ + borrower: _borrower2, + borrowerDebt: 17.218727143819483942 * 1e18, + borrowerCollateral: 3 * 1e18, + borrowert0Np: 5.255048076923076925 * 1e18, + borrowerCollateralization: 1.727860269914713433 * 1e18 + }); + assertEq(_quote.balanceOf(_lender), 46_999.772712801701582812 * 1e18); + _assertAuction( AuctionParams({ borrower: _borrower, @@ -553,19 +509,18 @@ contract ERC721PoolLiquidationsTakeTest is ERC721HelperContract { neutralPrice: 11.932577910666902372 * 1e18 }) ); - _assertKicker( - { - kicker: _lender, - claimable: 0, - locked: 0.227287198298417188 * 1e18 - } - ); + _assertKicker({ + kicker: _lender, + claimable: 0, + locked: 0.227287198298417188 * 1e18 + }); skip(10 hours); // before take: NFTs pledged by auctioned borrower are owned by the pool assertEq(_collateral.ownerOf(3), address(_pool)); assertEq(_collateral.ownerOf(1), address(_pool)); + // before take: check quote token balances of taker assertEq(_quote.balanceOf(_lender), 46_999.772712801701582812 * 1e18); @@ -585,36 +540,32 @@ contract ERC721PoolLiquidationsTakeTest is ERC721HelperContract { neutralPrice: 11.932577910666902372 * 1e18 }) ); - - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 23.014011023943546872 * 1e18, - borrowerCollateral: 2 * 1e18, - borrowert0Np: 10.404995192307692312 * 1e18, - borrowerCollateralization: 0.861838888763733724 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 23.014011023943546872 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 10.404995192307692312 * 1e18, + borrowerCollateralization: 0.861838888763733724 * 1e18 + }); /**************************************/ /*** Take all collateral tokens (2) ***/ /**************************************/ - _take( - { - from: _lender, - borrower: _borrower, - maxCollateral: 2, - bondChange: 0.014915722388333628 * 1e18, - givenAmount: 1.491572238833362816 * 1e18, - collateralTaken: 2 * 1e18, - isReward: true - } - ); + _take({ + from: _lender, + borrower: _borrower, + maxCollateral: 2, + bondChange: 0.014915722388333628 * 1e18, + givenAmount: 1.491572238833362816 * 1e18, + collateralTaken: 2 * 1e18, + isReward: true + }); // after take: NFTs pledged by liquidated borrower are owned by the taker assertEq(_collateral.ownerOf(3), _lender); assertEq(_collateral.ownerOf(1), _lender); + // after take : Taker quote token used for buying collateral assertEq(_quote.balanceOf(_lender), 46_998.281140562868219996 * 1e18); @@ -637,16 +588,13 @@ contract ERC721PoolLiquidationsTakeTest is ERC721HelperContract { ); // Borrower collateral is 0 and some debt is still to be paid - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 23.148335279174565965 * 1e18, - borrowerCollateral: 0, - borrowert0Np: 10.404995192307692312 * 1e18, - borrowerCollateralization: 0 - } - ); - + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 23.148335279174565965 * 1e18, + borrowerCollateral: 0, + borrowert0Np: 10.404995192307692312 * 1e18, + borrowerCollateralization: 0 + }); _assertAuction( AuctionParams({ borrower: _borrower, @@ -663,24 +611,20 @@ contract ERC721PoolLiquidationsTakeTest is ERC721HelperContract { neutralPrice: 11.932577910666902372 * 1e18 }) ); - // kicker bond is locked as auction is not cleared - _assertKicker( - { - kicker: _lender, - claimable: 0, - locked: 0.242202920686750816 * 1e18 - } - ); + _assertKicker({ + kicker: _lender, + claimable: 0, + locked: 0.242202920686750816 * 1e18 + }); - _settle( - { - from: _lender, - borrower: _borrower, - maxDepth: 10, - settledDebt: 20.183898781290497858 * 1e18 - } - ); + // settle auction + _settle({ + from: _lender, + borrower: _borrower, + maxDepth: 10, + settledDebt: 20.183898781290497858 * 1e18 + }); _assertAuction( AuctionParams({ @@ -698,18 +642,24 @@ contract ERC721PoolLiquidationsTakeTest is ERC721HelperContract { neutralPrice: 0 }) ); + _assertKicker({ + kicker: _lender, + claimable: 0.242202920686750816 * 1e18, + locked: 0 + }); - _assertKicker( - { - kicker: _lender, - claimable: 0.242202920686750816 * 1e18, - locked: 0 - } - ); + uint256 snapshot = vm.snapshot(); - // Kicker claims bond + reward changePrank(_lender); - _pool.withdrawBonds(); + + // Kicker claims bond + reward and transfer to a different address + _pool.withdrawBonds(_withdrawRecipient); + assertEq(_quote.balanceOf(_withdrawRecipient), 0.242202920686750816 * 1e18); + + vm.revertTo(snapshot); + + // Kicker claims bond + reward + _pool.withdrawBonds(_lender); assertEq(_quote.balanceOf(_lender), 46_998.523343483554970812 * 1e18); } @@ -733,37 +683,30 @@ contract ERC721PoolLiquidationsTakeTest is ERC721HelperContract { neutralPrice: 0 }) ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 22.728719829841718804 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 10.404995192307692312 * 1e18, + borrowerCollateralization: 0.872656701977127996 * 1e18 + }); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 22.728719829841718804 * 1e18, - borrowerCollateral: 2 * 1e18, - borrowert0Np: 10.404995192307692312 * 1e18, - borrowerCollateralization: 0.872656701977127996 * 1e18 - } - ); - - _kick( - { - from: _lender, - borrower: _borrower, - debt: 23.012828827714740289 * 1e18, - collateral: 2 * 1e18, - bond: 0.227287198298417188 * 1e18, - transferAmount: 0.227287198298417188 * 1e18 - } - ); + _kick({ + from: _lender, + borrower: _borrower, + debt: 23.012828827714740289 * 1e18, + collateral: 2 * 1e18, + bond: 0.227287198298417188 * 1e18, + transferAmount: 0.227287198298417188 * 1e18 + }); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 23.012828827714740289 * 1e18, - borrowerCollateral: 2 * 1e18, - borrowert0Np: 10.404995192307692312 * 1e18, - borrowerCollateralization: 0.861883162446546169 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 23.012828827714740289 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 10.404995192307692312 * 1e18, + borrowerCollateralization: 0.861883162446546169 * 1e18 + }); _assertAuction( AuctionParams({ borrower: _borrower, @@ -784,17 +727,16 @@ contract ERC721PoolLiquidationsTakeTest is ERC721HelperContract { // skip enough time to accumulate debt and take to not settle auction skip(50 hours); - _take( - { - from: _lender, - borrower: _borrower, - maxCollateral: 1, - bondChange: 0.000000000000006781 * 1e18, - givenAmount: 0.000000000000678144 * 1e18, - collateralTaken: 1 * 1e18, - isReward: true - } - ); + _take({ + from: _lender, + borrower: _borrower, + maxCollateral: 1, + bondChange: 0.000000000000006781 * 1e18, + givenAmount: 0.000000000000678144 * 1e18, + collateralTaken: 1 * 1e18, + isReward: true + }); + _assertAuction( AuctionParams({ borrower: _borrower, @@ -839,15 +781,23 @@ contract ERC721PoolLiquidationsTakeTest is ERC721HelperContract { neutralPrice: 0 }) ); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 0, - borrowerCollateral: 1 * 1e18, - borrowert0Np: 0, - borrowerCollateralization: 1 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 0, + borrowerCollateral: 1 * 1e18, + borrowert0Np: 0, + borrowerCollateralization: 1 * 1e18 + }); + + // borrower should be able to pull collateral from the pool + _repayDebtNoLupCheck({ + from: _borrower, + borrower: _borrower, + amountToRepay: 0, + amountRepaid: 0, + collateralToPull: 1 + }); + vm.revertTo(snapshot); // borrower repays part of debt, but not enough to exit from auction @@ -859,16 +809,15 @@ contract ERC721PoolLiquidationsTakeTest is ERC721HelperContract { collateralToPull: 0, newLup: _priceAt(3696) }); + // borrower pledge one more NFT to exit from auction uint256[] memory tokenIdsToAdd = new uint256[](1); tokenIdsToAdd[0] = 5; - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - tokenIds: tokenIdsToAdd - } - ); + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + tokenIds: tokenIdsToAdd + }); _assertAuction( AuctionParams({ @@ -886,14 +835,51 @@ contract ERC721PoolLiquidationsTakeTest is ERC721HelperContract { neutralPrice: 0 }) ); - _assertBorrower( - { - borrower: _borrower, - borrowerDebt: 19.630052245331353428 * 1e18, - borrowerCollateral: 2 * 1e18, - borrowert0Np: 8.902861174861655548 * 1e18, - borrowerCollateralization: 1.010408400292926569 * 1e18 - } - ); + _assertBorrower({ + borrower: _borrower, + borrowerDebt: 19.630052245331353428 * 1e18, + borrowerCollateral: 2 * 1e18, + borrowert0Np: 8.902861174861655548 * 1e18, + borrowerCollateralization: 1.010408400292926569 * 1e18 + }); + + } + + function testTakeCollateralWithAtomicSwapSubsetPool() external tearDown { + // Skip to make borrower undercollateralized + skip(1000 days); + + _kick({ + from: _lender, + borrower: _borrower, + debt: 23.012828827714740289 * 1e18, + collateral: 2 * 1e18, + bond: 0.227287198298417188 * 1e18, + transferAmount: 0.227287198298417188 * 1e18 + }); + + skip(5.5 hours); + + uint256 initialBalance = 10_000 * 1e18; + + // instantiate a NOOP taker contract which implements IERC721Taker + NFTNoopTakeExample taker = new NFTNoopTakeExample(); + deal(address(_quote), address(taker), initialBalance); + changePrank(address(taker)); + _quote.approve(address(_pool), type(uint256).max); + + bytes memory data = abi.encode(address(_pool)); + _pool.take(_borrower, 2, address(taker), data); + + // check that token ids are the same as id pledged by borrower + assertEq(taker.tokenIdsReceived(0), 3); + assertEq(taker.tokenIdsReceived(1), 1); + + // check that the amount of quote tokens passed to taker contract is the same as the one deducted from taker balance + uint256 currentBalance = _quote.balanceOf(address(taker)); + assertEq(initialBalance - taker.quoteAmountDueReceived(), currentBalance); + + // check address received is the address of current ajna pool + assertEq(taker.poolAddressReceived(), address(_pool)); } } \ No newline at end of file diff --git a/tests/forge/ERC721Pool/ERC721PoolPurchaseQuote.t.sol b/tests/forge/ERC721Pool/ERC721PoolPurchaseQuote.t.sol index 18a868850..a4c80af8f 100644 --- a/tests/forge/ERC721Pool/ERC721PoolPurchaseQuote.t.sol +++ b/tests/forge/ERC721Pool/ERC721PoolPurchaseQuote.t.sol @@ -48,15 +48,13 @@ contract ERC721PoolPurchaseQuoteTest is ERC721HelperContract { assertEq(_priceAtTestIndex, 3_010.892022197881557845 * 1e18); // check bucket state - _assertBucket( - { - index: testIndex, - lpBalance: 0, - collateral: 0, - deposit: 0, - exchangeRate: 1 * 1e27 - } - ); + _assertBucket({ + index: testIndex, + lpBalance: 0, + collateral: 0, + deposit: 0, + exchangeRate: 1 * 1e18 + }); // check pool state assertEq(_collateral.balanceOf(_bidder), 13); @@ -66,59 +64,50 @@ contract ERC721PoolPurchaseQuoteTest is ERC721HelperContract { assertEq(_quote.balanceOf(_bidder), 0); // lender adds initial quote to pool - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: testIndex - } - ); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: testIndex + }); // check bucket state - _assertBucket( - { - index: testIndex, - lpBalance: 10_000 * 1e27, - collateral: 0, - deposit: 10_000 * 1e18, - exchangeRate: 1 * 1e27 - } - ); + _assertBucket({ + index: testIndex, + lpBalance: 10_000 * 1e18, + collateral: 0, + deposit: 10_000 * 1e18, + exchangeRate: 1 * 1e18 + }); // _bidder deposits collateral into a bucket changePrank(_bidder); + uint256[] memory tokenIdsToAdd = new uint256[](3); tokenIdsToAdd[0] = 65; tokenIdsToAdd[1] = 70; tokenIdsToAdd[2] = 73; - uint256 lpBalanceChange = _addCollateral( - { - from: _bidder, - tokenIds: tokenIdsToAdd, - index: testIndex, - lpAward: 9_032.676066593644673535 * 1e27 - } - ); + uint256 lpBalanceChange = _addCollateral({ + from: _bidder, + tokenIds: tokenIdsToAdd, + index: testIndex, + lpAward: 9_032.676066593644673535 * 1e18 + }); // check bucket state - _assertBucket( - { - index: testIndex, - lpBalance: 19_032.676066593644673535 * 1e27, - collateral: Maths.wad(3), - deposit: 10_000 * 1e18, - exchangeRate: 1 * 1e27 - } - ); - _assertLenderLpBalance( - { - lender: _bidder, - index: testIndex, - lpBalance: lpBalanceChange, - depositTime: _startTime - } - ); + _assertBucket({ + index: testIndex, + lpBalance: 19_032.676066593644673535 * 1e18, + collateral: Maths.wad(3), + deposit: 10_000 * 1e18, + exchangeRate: 1 * 1e18 + }); + _assertLenderLpBalance({ + lender: _bidder, + index: testIndex, + lpBalance: lpBalanceChange, + depositTime: _startTime + }); // check pool state assertEq(_collateral.balanceOf(_bidder), 10); @@ -129,76 +118,65 @@ contract ERC721PoolPurchaseQuoteTest is ERC721HelperContract { // bidder removes quote token from bucket skip(1 days); // skip to avoid penalty + uint256 qtToRemove = Maths.wmul(_priceAtTestIndex, 3 * 1e18); - _removeAllLiquidity( - { - from: _bidder, - amount: qtToRemove, - index: testIndex, - newLup: _lup(), - lpRedeem: 9_032.676066593644673535 * 1e27 - } - ); + + _removeAllLiquidity({ + from: _bidder, + amount: qtToRemove, + index: testIndex, + newLup: _lup(), + lpRedeem: 9_032.676066593644673535 * 1e18 + }); + assertEq(_quote.balanceOf(_bidder), qtToRemove); - _assertBucket( - { - index: testIndex, - lpBalance: 10_000 * 1e27, - collateral: Maths.wad(3), - deposit: 967.323933406355326465 * 1e18, - exchangeRate: 1 * 1e27 - } - ); - _assertLenderLpBalance( - { - lender: _bidder, - index: testIndex, - lpBalance: 0, - depositTime: _startTime - } - ); + _assertBucket({ + index: testIndex, + lpBalance: 10_000 * 1e18, + collateral: Maths.wad(3), + deposit: 967.323933406355326465 * 1e18, + exchangeRate: 1 * 1e18 + }); + _assertLenderLpBalance({ + lender: _bidder, + index: testIndex, + lpBalance: 0, + depositTime: _startTime + }); // lender removes all collateral from bucket - _removeCollateral( - { - from: _lender, - amount: 3, - index: testIndex, - lpRedeem: 9_032.676066593644673535 * 1e27 - } - ); - - _assertBucket( - { - index: testIndex, - lpBalance: 967.323933406355326465 * 1e27, - collateral: 0, - deposit: 967.323933406355326465 * 1e18, - exchangeRate: 1 * 1e27 - } - ); + _removeCollateral({ + from: _lender, + amount: 3, + index: testIndex, + lpRedeem: 9_032.676066593644673535 * 1e18 + }); + + _assertBucket({ + index: testIndex, + lpBalance: 967.323933406355326465 * 1e18, + collateral: 0, + deposit: 967.323933406355326465 * 1e18, + exchangeRate: 1 * 1e18 + }); // lender removes remaining quote token to empty the bucket - _removeAllLiquidity( - { - from: _lender, - amount: 967.323933406355326465 * 1e18, - index: testIndex, - newLup: _lup(), - lpRedeem: 967.323933406355326465 * 1e27 - } - ); - - _assertBucket( - { - index: testIndex, - lpBalance: 0, - collateral: 0, - deposit: 0, - exchangeRate: 1 * 1e27 - } - ); + _removeAllLiquidity({ + from: _lender, + amount: 967.323933406355326465 * 1e18, + index: testIndex, + newLup: _lup(), + lpRedeem: 967.323933406355326465 * 1e18 + }); + + _assertBucket({ + index: testIndex, + lpBalance: 0, + collateral: 0, + deposit: 0, + exchangeRate: 1 * 1e18 + }); } /** @@ -210,42 +188,32 @@ contract ERC721PoolPurchaseQuoteTest is ERC721HelperContract { */ function testSubsetPurchaseQuoteWithDebt() external tearDown { // lenders add liquidity - _addInitialLiquidity( - { - from: _lender, - amount: 20_000 * 1e18, - index: 2350 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 2351 - } - ); - _addInitialLiquidity( - { - from: _lender, - amount: 10_000 * 1e18, - index: 2352 - } - ); - - _addInitialLiquidity( - { - from: _lender2, - amount: 4_000 * 1e18, - index: 2350 - } - ); - _addInitialLiquidity( - { - from: _lender2, - amount: 5_000 * 1e18, - index: 2352 - } - ); + _addInitialLiquidity({ + from: _lender, + amount: 20_000 * 1e18, + index: 2350 + }); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2351 + }); + _addInitialLiquidity({ + from: _lender, + amount: 10_000 * 1e18, + index: 2352 + }); + + _addInitialLiquidity({ + from: _lender2, + amount: 4_000 * 1e18, + index: 2350 + }); + _addInitialLiquidity({ + from: _lender2, + amount: 5_000 * 1e18, + index: 2352 + }); skip(3600); @@ -255,34 +223,30 @@ contract ERC721PoolPurchaseQuoteTest is ERC721HelperContract { tokenIdsToAdd[1] = 3; tokenIdsToAdd[2] = 5; tokenIdsToAdd[3] = 51; - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - tokenIds: tokenIdsToAdd - } - ); - _borrow( - { - from: _borrower, - amount: 24_000 * 1e18, - indexLimit: 2_351, - newLup: 8_123.467933811934300919 * 1e18 - } - ); + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + tokenIds: tokenIdsToAdd + }); + _borrow({ + from: _borrower, + amount: 24_000 * 1e18, + indexLimit: 2_351, + newLup: 8_123.467933811934300919 * 1e18 + }); + assertEq(_lup(), _priceAt(2351)); + skip(86400); // check bucket state - _assertBucket( - { - index: 2350, - lpBalance: 24_000 * 1e27, - collateral: 0, - deposit: 24_000 * 1e18, - exchangeRate: 1 * 1e27 - } - ); + _assertBucket({ + index: 2350, + lpBalance: 24_000 * 1e18, + collateral: 0, + deposit: 24_000 * 1e18, + exchangeRate: 1 * 1e18 + }); // bidder purchases all quote from the highest bucket tokenIdsToAdd = new uint256[](4); @@ -292,102 +256,87 @@ contract ERC721PoolPurchaseQuoteTest is ERC721HelperContract { tokenIdsToAdd[3] = 74; uint256 amountToPurchase = 10_100 * 1e18; assertGt(_quote.balanceOf(address(_pool)), amountToPurchase); + uint256 amountWithInterest = 24_002.749104114061152000 * 1e18; - _addCollateral( - { - from: _bidder, - tokenIds: tokenIdsToAdd, - index: 2350, - lpAward: 32_654.410675370944354984500292928 * 1e27 - } - ); + _addCollateral({ + from: _bidder, + tokenIds: tokenIdsToAdd, + index: 2350, + lpAward: 32_654.410675370944354985 * 1e18 + }); + skip(25 hours); // remove liquidity after one day to avoid early withdraw penalty - _removeAllLiquidity( - { - from: _bidder, - amount: amountWithInterest, - index: 2350, - newLup: _priceAt(2352), - lpRedeem: 24_000.766696558404292700773653981 * 1e27 - } - ); + + _removeAllLiquidity({ + from: _bidder, + amount: amountWithInterest, + index: 2350, + newLup: _priceAt(2352), + lpRedeem: 24_000.766696558404301151 * 1e18 + }); assertEq(_quote.balanceOf(_bidder), amountWithInterest); // check bucket state - _assertBucket( - { - index: 2350, - lpBalance: 32_653.643978812540062283726638947 * 1e27, - collateral: Maths.wad(4), - deposit: 0, - exchangeRate: 1.000082597676179283352120528 * 1e27 - } - ); + _assertBucket({ + index: 2350, + lpBalance: 32_653.643978812540053834 * 1e18, + collateral: Maths.wad(4), + deposit: 0, + exchangeRate: 1.000082597676179284 * 1e18 + }); // bidder withdraws unused collateral - (uint256 amount) = _removeCollateral( - { - from: _bidder, - amount: 1, - index: 2350, - lpRedeem: 8_163.410994703135015570931665340 * 1e27 - } - ); - - _assertLenderLpBalance( - { - lender: _bidder, - index: 2350, - lpBalance: 490.232984109405046712794973607 * 1e27, - depositTime: _startTime + 25 hours - } - ); + (uint256 amount) = _removeCollateral({ + from: _bidder, + amount: 1, + index: 2350, + lpRedeem: 8_163.410994703135010282 * 1e18 + }); + + _assertLenderLpBalance({ + lender: _bidder, + index: 2350, + lpBalance: 490.232984109405043552 * 1e18, + depositTime: _startTime + 25 hours + }); skip(7200); changePrank(_lender); // lender exchanges their lp for collateral - (amount) = _removeCollateral( - { - from: _lender, - amount: 1, - index: 2350, - lpRedeem: 8_163.410994703135015570931665340 * 1e27 - } - ); - - _assertLenderLpBalance( - { - lender: _bidder, - index: 2350, - lpBalance: 490.232984109405046712794973607 * 1e27, - depositTime: _startTime + 25 hours - } - ); + (amount) = _removeCollateral({ + from: _lender, + amount: 1, + index: 2350, + lpRedeem: 8_163.410994703135018445 * 1e18 + }); + + _assertLenderLpBalance({ + lender: _bidder, + index: 2350, + lpBalance: 490.232984109405043552 * 1e18, + depositTime: _startTime + 25 hours + }); skip(3600); // check bucket state - _assertBucket( - { - index: 2350, - lpBalance: 16_326.821989406270031141863308267 * 1e27, - collateral: Maths.wad(2), - deposit: 0, - exchangeRate: 1.000082597676179283352120529 * 1e27 - } - ); + _assertBucket({ + index: 2350, + lpBalance: 16_326.821989406270025107 * 1e18, + collateral: Maths.wad(2), + deposit: 0, + exchangeRate: 1.000082597676179284 * 1e18 + }); // should revert if lender2 attempts to remove more collateral than lp is available for - _assertRemoveCollateralInsufficientLPsRevert( - { - from: _lender2, - amount: 1, - index: 2350 - } - ); + _assertRemoveCollateralInsufficientLPsRevert({ + from: _lender2, + amount: 1, + index: 2350 + }); skip(3600); } diff --git a/tests/forge/ERC721Pool/ERC721PoolReserveAuction.t.sol b/tests/forge/ERC721Pool/ERC721PoolReserveAuction.t.sol index 8a93dd435..ccbba4718 100644 --- a/tests/forge/ERC721Pool/ERC721PoolReserveAuction.t.sol +++ b/tests/forge/ERC721Pool/ERC721PoolReserveAuction.t.sol @@ -27,57 +27,51 @@ contract ERC721PoolReserveAuctionTest is ERC721HelperContract { // lender adds liquidity and borrower draws debt uint16 bucketId = 1663; - _addInitialLiquidity( - { - from: _lender, - amount: 200_000 * 1e18, - index: bucketId - } - ); + + _addInitialLiquidity({ + from: _lender, + amount: 200_000 * 1e18, + index: bucketId + }); // borrower draws debt uint256[] memory tokenIdsToAdd = new uint256[](1); tokenIdsToAdd[0] = 1; - _pledgeCollateral( - { - from: _borrower, - borrower: _borrower, - tokenIds: tokenIdsToAdd - } - ); - _borrow( - { - from: _borrower, - amount: 175_000 * 1e18, - indexLimit: bucketId, - newLup: 251_183.992399245533703810 * 1e18 - } - ); + _pledgeCollateral({ + from: _borrower, + borrower: _borrower, + tokenIds: tokenIdsToAdd + }); + _borrow({ + from: _borrower, + amount: 175_000 * 1e18, + indexLimit: bucketId, + newLup: 251_183.992399245533703810 * 1e18 + }); + (uint256 poolDebt,,) = _pool.debtInfo(); assertEq(poolDebt - 175_000 * 1e18, 168.26923076923085 * 1e18); + skip(26 weeks); + (poolDebt,,) = _pool.debtInfo(); assertEq(poolDebt - 175_000 * 1e18, 4_590.373946590638353626 * 1e18); // debt matches develop } function testClaimableReserveNoAuction() external { // ensure empty state is returned - _assertReserveAuction( - { - reserves: 168.26923076923085 * 1e18, - claimableReserves : 0, - claimableReservesRemaining: 0, - auctionPrice: 0, - timeRemaining: 0 - } - ); + _assertReserveAuction({ + reserves: 168.26923076923085 * 1e18, + claimableReserves : 0, + claimableReservesRemaining: 0, + auctionPrice: 0, + timeRemaining: 0 + }); // ensure cannot take when no auction was started - _assertTakeReservesNoAuctionRevert( - { - amount: 555 * 1e18 - } - ); + _assertTakeReservesNoAuctionRevert({ + amount: 555 * 1e18 + }); } function testUnclaimableReserves() external { @@ -91,16 +85,16 @@ contract ERC721PoolReserveAuctionTest is ERC721HelperContract { newLup: 251_183.992399245533703810 * 1e18 }); - _assertReserveAuction( - { - reserves: 499.181304561658553626 * 1e18, - claimableReserves : 0, - claimableReservesRemaining: 0, - auctionPrice: 0, - timeRemaining: 0 - } - ); + _assertReserveAuction({ + reserves: 499.181304561658553626 * 1e18, + claimableReserves : 0, + claimableReservesRemaining: 0, + auctionPrice: 0, + timeRemaining: 0 + }); + changePrank(_bidder); + _assertTakeReservesNoReservesRevert(); } @@ -114,57 +108,70 @@ contract ERC721PoolReserveAuctionTest is ERC721HelperContract { collateralToPull: 0, newLup: MAX_PRICE }); - _assertReserveAuction( - { - reserves: 499.181304561658553626 * 1e18, - claimableReserves : 499.181304561658553626 * 1e18, - claimableReservesRemaining: 0, - auctionPrice: 0, - timeRemaining: 0 - } - ); + + _assertReserveAuction({ + reserves: 499.181304561658553626 * 1e18, + claimableReserves : 499.181304561658553626 * 1e18, + claimableReservesRemaining: 0, + auctionPrice: 0, + timeRemaining: 0 + }); // kick off a new auction - _startClaimableReserveAuction( - { - from: _bidder, - remainingReserves: 494.189491516041968090 * 1e18, - price: 1_000_000_000 * 1e18 - } - ); + _startClaimableReserveAuction({ + from: _bidder, + remainingReserves: 494.189491516041968090 * 1e18, + price: 1_000_000_000 * 1e18 + }); + _assertReserveAuctionPrice(1_000_000_000 * 1e18); // check prices skip(37 minutes); _assertReserveAuctionPrice(652176034.882782126826643053 * 1e18); + skip(23 hours); // 23 hours 37 minutes _assertReserveAuctionPrice(77.745441780421987394 * 1e18); + skip(1400); // 24 hours 0 minutes 20 seconds _assertReserveAuctionPrice(59.604644775390625 * 1e18); + skip(100); // 24 hours 2 minutes _assertReserveAuctionPrice(58.243272807255146201 * 1e18); + skip(58 minutes); // 25 hours _assertReserveAuctionPrice(29.8023223876953125 * 1e18); + skip(5 hours); // 30 hours _assertReserveAuctionPrice(0.931322574615478515 * 1e18); + skip(121 minutes); // 32 hours 1 minute _assertReserveAuctionPrice(0.230156355619639189 * 1e18); + skip(7700 seconds); // 34 hours 9 minutes 20 seconds _assertReserveAuctionPrice(0.052459681325756842 * 1e18); + skip(8 hours); // 42 hours 9 minutes 20 seconds _assertReserveAuctionPrice(0.000204920630178738 * 1e18); + skip(6 hours); // 42 hours 9 minutes 20 seconds _assertReserveAuctionPrice(0.000003201884846542 * 1e18); + skip(3100 seconds); // 43 hours _assertReserveAuctionPrice(0.000001755953640897 * 1e18); + skip(5 hours); // 48 hours _assertReserveAuctionPrice(0.000000054873551278 * 1e18); + skip(12 hours); // 60 hours _assertReserveAuctionPrice(0.000000000013396863 * 1e18); + skip(11 hours); // 71 hours _assertReserveAuctionPrice(0.000000000000006541 * 1e18); + skip(3599 seconds); // 71 hours 59 minutes 59 seconds _assertReserveAuctionPrice(0.000000000000003308 * 1e18); + skip(1 seconds); // 72 hours _assertReserveAuctionPrice(0.000000000000003270 * 1e18); } diff --git a/tests/forge/FenwickTree.t.sol b/tests/forge/FenwickTree.t.sol index 30359e338..49af985aa 100644 --- a/tests/forge/FenwickTree.t.sol +++ b/tests/forge/FenwickTree.t.sol @@ -109,11 +109,11 @@ contract FenwickTreeTest is DSTestPlus { function testFenwickFuzzyScalingPrefix( uint256 insertions_, uint256 totalAmount_, + uint256 seed_, uint256 scaleIndex_, uint256 factor_ - ) external { - - _tree.fuzzyFill(insertions_, totalAmount_, false); + ) external { + _tree.fuzzyFill(insertions_, totalAmount_, seed_, false); uint256 scaleIndex = bound(scaleIndex_, 2, MAX_INDEX); uint256 subIndex = randomInRange(1, scaleIndex - 1); @@ -158,11 +158,11 @@ contract FenwickTreeTest is DSTestPlus { function testLoadFenwickFuzzyScalingFind( uint256 insertions_, uint256 totalAmount_, + uint256 seed_, uint256 scaleIndex_, uint256 factor_ - ) external { - - _tree.fuzzyFill(insertions_, totalAmount_, false); + ) external { + _tree.fuzzyFill(insertions_, totalAmount_, seed_, false); uint256 scaleIndex = bound(scaleIndex_, 2, 7388); uint256 subIndex = randomInRange(0, scaleIndex - 1); @@ -190,10 +190,10 @@ contract FenwickTreeTest is DSTestPlus { */ function testLoadFenwickFuzzyRemoval( uint256 insertions_, - uint256 totalAmount_ - ) external { - - _tree.fuzzyFill(insertions_, totalAmount_, true); + uint256 totalAmount_, + uint256 seed_ + ) external { + _tree.fuzzyFill(insertions_, totalAmount_, seed_, true); // get Index randombly uint256 removalIndex = _tree.getIByInsertIndex(randomInRange(0, _tree.numInserts() - 1)); @@ -213,7 +213,6 @@ contract FenwickTreeTest is DSTestPlus { assertEq(preRemovalTreeSum - removalAmount, _tree.treeSum()); assertEq(preRemovalParentIndexSum - removalAmount, postRemovalParentIndexSum); } - } contract FenwickTreeGasLoadTest is DSTestPlus { diff --git a/tests/forge/Heap.t.sol b/tests/forge/Heap.t.sol index d537d0d70..b2f93ee85 100644 --- a/tests/forge/Heap.t.sol +++ b/tests/forge/Heap.t.sol @@ -231,10 +231,10 @@ contract HeapTest is DSTestPlus { assertEq(_loans.getTotalTps(), 7); } - function testLoadHeapFuzzy(uint256 inserts_) public { + function testLoadHeapFuzzy(uint256 inserts_, uint256 seed_) public { // test adding different TPs - _loans.fuzzyFill(inserts_, true); + _loans.fuzzyFill(inserts_, seed_, true); // test adding different TPs address removeAddress = _loans.getIdByInsertIndex(randomInRange(1, _loans.numInserts() - 1, true)); diff --git a/tests/forge/MathTest.t.sol b/tests/forge/MathTest.t.sol index e59e894d9..51291ba55 100644 --- a/tests/forge/MathTest.t.sol +++ b/tests/forge/MathTest.t.sol @@ -21,7 +21,6 @@ contract MathTest is DSTestPlus { function testZeroStaysZero() external { assertEq(Maths.rayToWad(0), 0); - assertEq(Maths.wadToRay(0), 0); } function testMultiplication() external { @@ -31,31 +30,6 @@ contract MathTest is DSTestPlus { assertEq(debt * inflator, 10_213.6546200311111065616975993 * 1e45); } - function testDivision() external { - uint256 debt = 11_000.143012091382543917 * 1e18; - uint256 price = 1_001.6501589292607751220 * 1e18; - - assertEq(Maths.wdiv(debt, price), 10.98202093218880245 * 1e18); - assertEq(debt * 1e18 / price, 10.98202093218880245 * 1e18); - assertEq(Maths.wwdivr(debt, price), 10.982020932188802450191601163 * 1e27); - - uint256 exchangeRate = 1.09232010 * 1e27; - assertEq(Maths.rdiv(Maths.wadToRay(debt), exchangeRate), Maths.wrdivr(debt, exchangeRate)); - - uint256 lpBalance = 36_900.58124 * 1e27; - uint256 lpRedemption = Maths.rdiv(lpBalance, exchangeRate); - assertEq(Maths.rayToWad(lpRedemption), Maths.rrdivw(lpBalance, exchangeRate)); - assertEq(Maths.rayToWad(Maths.rdiv(lpRedemption, Maths.wadToRay(price))), Maths.rwdivw(lpRedemption, price)); - - uint256 claimableCollateral1 = Maths.rwdivw(Maths.rdiv(lpBalance, exchangeRate), price); // rounds - uint256 claimableCollateral2 = lpBalance * 1e36 / exchangeRate / price; // truncates - assertEq(claimableCollateral1, 33.726184963566645999 * 1e18); - assertEq(claimableCollateral2, 33.726184963566645998 * 1e18); - - assertEq(Maths.wdiv(1 * 1e18, 60 * 1e18), 0.016666666666666667 * 1e18); - assertEq(Maths.rdiv(1 * 1e27, 3 * 1e27), 0.333333333333333333333333333 * 1e27); - } - function testScaleConversions() external { assertEq(Maths.wad(153), 153 * 1e18); } diff --git a/tests/forge/PositionManager.t.sol b/tests/forge/PositionManager.t.sol index 5e691fc0f..5545bf3bf 100644 --- a/tests/forge/PositionManager.t.sol +++ b/tests/forge/PositionManager.t.sol @@ -27,6 +27,7 @@ abstract contract PositionManagerERC20PoolHelperContract is ERC20HelperContract vm.prank(operator_); _quote.approve(address(_pool), type(uint256).max); + vm.prank(operator_); _quote.approve(address(_positionManager), type(uint256).max); } @@ -42,7 +43,7 @@ abstract contract PositionManagerERC20PoolHelperContract is ERC20HelperContract } function _getPermitSig( - address receiver_, + address spender_, uint256 tokenId_, uint256 deadline_, uint256 ownerPrivateKey_ @@ -56,7 +57,7 @@ abstract contract PositionManagerERC20PoolHelperContract is ERC20HelperContract keccak256( abi.encode( _positionManager.PERMIT_TYPEHASH(), - receiver_, + spender_, tokenId_, 0, deadline_ @@ -90,11 +91,11 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract require(tokenId != 0, "tokenId nonce not incremented"); // check position info - address owner = _positionManager.ownerOf(tokenId); - uint256 lpTokens = _positionManager.getLPTokens(tokenId, mintPrice); + address owner = _positionManager.ownerOf(tokenId); + uint256 lps = _positionManager.getLPs(tokenId, mintPrice); assertEq(owner, testAddress); - assertEq(lpTokens, 0); + assertEq(lps, 0); // deploy a new factory to simulate creating a pool outside of expected factories ERC20PoolFactory invalidFactory = new ERC20PoolFactory(_ajna); @@ -107,10 +108,10 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract /** * @notice Tests attachment of a created position to an already existing NFT. - * LP tokens are checked to verify ownership of position. + * LPs are checked to verify ownership of position. * Reverts: - * Attempts to memorialize when lp tokens aren't allowed to be transfered. - * Attempts to set position owner when not owner of the LP tokens. + * Attempts to memorialize when lps aren't allowed to be transfered. + * Attempts to set position owner when not owner of the LPs. */ function testMemorializePositions() external { address testAddress = makeAddr("testAddress"); @@ -124,27 +125,21 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract indexes[1] = 2551; indexes[2] = 2552; - _addInitialLiquidity( - { - from: testAddress, - amount: 3_000 * 1e18, - index: indexes[0] - } - ); - _addInitialLiquidity( - { - from: testAddress, - amount: 3_000 * 1e18, - index: indexes[1] - } - ); - _addInitialLiquidity( - { - from: testAddress, - amount: 3_000 * 1e18, - index: indexes[2] - } - ); + _addInitialLiquidity({ + from: testAddress, + amount: 3_000 * 1e18, + index: indexes[0] + }); + _addInitialLiquidity({ + from: testAddress, + amount: 3_000 * 1e18, + index: indexes[1] + }); + _addInitialLiquidity({ + from: testAddress, + amount: 3_000 * 1e18, + index: indexes[2] + }); // mint an NFT to later memorialize existing positions into uint256 tokenId = _mintNFT(testAddress, testAddress, address(_pool)); @@ -162,24 +157,24 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract _positionManager.memorializePositions(memorializeParams); // allow position manager to take ownership of the position - _pool.approveLpOwnership(address(_positionManager), indexes[0], 3_000 * 1e27); - _pool.approveLpOwnership(address(_positionManager), indexes[1], 3_000 * 1e27); - _pool.approveLpOwnership(address(_positionManager), indexes[2], 3_000 * 1e27); + _pool.approveLpOwnership(address(_positionManager), indexes[0], 3_000 * 1e18); + _pool.approveLpOwnership(address(_positionManager), indexes[1], 3_000 * 1e18); + _pool.approveLpOwnership(address(_positionManager), indexes[2], 3_000 * 1e18); // memorialize quote tokens into minted NFT vm.expectEmit(true, true, true, true); emit MemorializePosition(testAddress, tokenId); vm.expectEmit(true, true, true, true); - emit TransferLPTokens(testAddress, address(_positionManager), indexes, 9_000 * 1e27); + emit TransferLPs(testAddress, address(_positionManager), indexes, 9_000 * 1e18); _positionManager.memorializePositions(memorializeParams); // check memorialization success - uint256 positionAtPriceOneLPTokens = _positionManager.getLPTokens(tokenId, indexes[0]); - assertGt(positionAtPriceOneLPTokens, 0); + uint256 positionAtPriceOneLPs = _positionManager.getLPs(tokenId, indexes[0]); + assertGt(positionAtPriceOneLPs, 0); - // check lp tokens at non added to price - uint256 positionAtWrongPriceLPTokens = _positionManager.getLPTokens(tokenId, 4000000 * 1e18); - assertEq(positionAtWrongPriceLPTokens, 0); + // check lps at non added to price + uint256 positionAtWrongPriceLPs = _positionManager.getLPs(tokenId, 4000000 * 1e18); + assertEq(positionAtWrongPriceLPs, 0); assertTrue(_positionManager.isIndexInPosition(tokenId, 2550)); assertTrue(_positionManager.isIndexInPosition(tokenId, 2551)); @@ -198,85 +193,67 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract indexes[1] = 2551; indexes[2] = 2552; - _addInitialLiquidity( - { - from: testAddress, - amount: 3_000 * 1e18, - index: indexes[0] - } - ); - _addInitialLiquidity( - { - from: testAddress, - amount: 3_000 * 1e18, - index: indexes[1] - } - ); - _addInitialLiquidity( - { - from: testAddress, - amount: 3_000 * 1e18, - index: indexes[2] - } - ); + _addInitialLiquidity({ + from: testAddress, + amount: 3_000 * 1e18, + index: indexes[0] + }); + _addInitialLiquidity({ + from: testAddress, + amount: 3_000 * 1e18, + index: indexes[1] + }); + _addInitialLiquidity({ + from: testAddress, + amount: 3_000 * 1e18, + index: indexes[2] + }); // mint an NFT to later memorialize existing positions into uint256 tokenId = _mintNFT(testAddress, testAddress, address(_pool)); // check LPs - _assertLenderLpBalance( - { - lender: testAddress, - index: indexes[0], - lpBalance: 3_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: indexes[0], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: testAddress, - index: indexes[1], - lpBalance: 3_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: indexes[1], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: testAddress, - index: indexes[2], - lpBalance: 3_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: indexes[2], - lpBalance: 0, - depositTime: 0 - } - ); + _assertLenderLpBalance({ + lender: testAddress, + index: indexes[0], + lpBalance: 3_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: indexes[0], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: testAddress, + index: indexes[1], + lpBalance: 3_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: indexes[1], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: testAddress, + index: indexes[2], + lpBalance: 3_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: indexes[2], + lpBalance: 0, + depositTime: 0 + }); // check position manager state - assertEq(_positionManager.getLPTokens(tokenId, indexes[0]), 0); - assertEq(_positionManager.getLPTokens(tokenId, indexes[1]), 0); - assertEq(_positionManager.getLPTokens(tokenId, indexes[2]), 0); + assertEq(_positionManager.getLPs(tokenId, indexes[0]), 0); + assertEq(_positionManager.getLPs(tokenId, indexes[1]), 0); + assertEq(_positionManager.getLPs(tokenId, indexes[2]), 0); assertFalse(_positionManager.isIndexInPosition(tokenId, indexes[0])); assertFalse(_positionManager.isIndexInPosition(tokenId, indexes[1])); assertFalse(_positionManager.isIndexInPosition(tokenId, indexes[2])); @@ -286,221 +263,179 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract tokenId, indexes ); // allow position manager to take ownership of the position - _pool.approveLpOwnership(address(_positionManager), indexes[0], 3_000 * 1e27); - _pool.approveLpOwnership(address(_positionManager), indexes[1], 3_000 * 1e27); - _pool.approveLpOwnership(address(_positionManager), indexes[2], 3_000 * 1e27); + _pool.approveLpOwnership(address(_positionManager), indexes[0], 3_000 * 1e18); + _pool.approveLpOwnership(address(_positionManager), indexes[1], 3_000 * 1e18); + _pool.approveLpOwnership(address(_positionManager), indexes[2], 3_000 * 1e18); // memorialize quote tokens into minted NFT vm.expectEmit(true, true, true, true); emit MemorializePosition(testAddress, tokenId); vm.expectEmit(true, true, true, true); - emit TransferLPTokens(testAddress, address(_positionManager), indexes, 9_000 * 1e27); + emit TransferLPs(testAddress, address(_positionManager), indexes, 9_000 * 1e18); _positionManager.memorializePositions(memorializeParams); - _assertLenderLpBalance( - { - lender: testAddress, - index: indexes[0], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: indexes[0], - lpBalance: 3_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: testAddress, - index: indexes[1], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: indexes[1], - lpBalance: 3_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: testAddress, - index: indexes[2], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: indexes[2], - lpBalance: 3_000 * 1e27, - depositTime: _startTime - } - ); + _assertLenderLpBalance({ + lender: testAddress, + index: indexes[0], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: indexes[0], + lpBalance: 3_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: testAddress, + index: indexes[1], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: indexes[1], + lpBalance: 3_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: testAddress, + index: indexes[2], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: indexes[2], + lpBalance: 3_000 * 1e18, + depositTime: _startTime + }); // check position manager state - assertEq(_positionManager.getLPTokens(tokenId, indexes[0]), 3_000 * 1e27); - assertEq(_positionManager.getLPTokens(tokenId, indexes[1]), 3_000 * 1e27); - assertEq(_positionManager.getLPTokens(tokenId, indexes[2]), 3_000 * 1e27); + assertEq(_positionManager.getLPs(tokenId, indexes[0]), 3_000 * 1e18); + assertEq(_positionManager.getLPs(tokenId, indexes[1]), 3_000 * 1e18); + assertEq(_positionManager.getLPs(tokenId, indexes[2]), 3_000 * 1e18); assertTrue(_positionManager.isIndexInPosition(tokenId, indexes[0])); assertTrue(_positionManager.isIndexInPosition(tokenId, indexes[1])); assertTrue(_positionManager.isIndexInPosition(tokenId, indexes[2])); // add more liquidity - _addInitialLiquidity( - { - from: testAddress, - amount: 1_000 * 1e18, - index: indexes[0] - } - ); - _addInitialLiquidity( - { - from: testAddress, - amount: 2_000 * 1e18, - index: indexes[1] - } - ); - _addInitialLiquidity( - { - from: testAddress, - amount: 3_000 * 1e18, - index: indexes[2] - } - ); + _addInitialLiquidity({ + from: testAddress, + amount: 1_000 * 1e18, + index: indexes[0] + }); + _addInitialLiquidity({ + from: testAddress, + amount: 2_000 * 1e18, + index: indexes[1] + }); + _addInitialLiquidity({ + from: testAddress, + amount: 3_000 * 1e18, + index: indexes[2] + }); // check LP balance - _assertLenderLpBalance( - { - lender: testAddress, - index: indexes[0], - lpBalance: 1_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: indexes[0], - lpBalance: 3_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: testAddress, - index: indexes[1], - lpBalance: 2_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: indexes[1], - lpBalance: 3_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: testAddress, - index: indexes[2], - lpBalance: 3_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: indexes[2], - lpBalance: 3_000 * 1e27, - depositTime: _startTime - } - ); + _assertLenderLpBalance({ + lender: testAddress, + index: indexes[0], + lpBalance: 1_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: indexes[0], + lpBalance: 3_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: testAddress, + index: indexes[1], + lpBalance: 2_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: indexes[1], + lpBalance: 3_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: testAddress, + index: indexes[2], + lpBalance: 3_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: indexes[2], + lpBalance: 3_000 * 1e18, + depositTime: _startTime + }); // check position manager state - assertEq(_positionManager.getLPTokens(tokenId, indexes[0]), 3_000 * 1e27); - assertEq(_positionManager.getLPTokens(tokenId, indexes[1]), 3_000 * 1e27); - assertEq(_positionManager.getLPTokens(tokenId, indexes[2]), 3_000 * 1e27); + assertEq(_positionManager.getLPs(tokenId, indexes[0]), 3_000 * 1e18); + assertEq(_positionManager.getLPs(tokenId, indexes[1]), 3_000 * 1e18); + assertEq(_positionManager.getLPs(tokenId, indexes[2]), 3_000 * 1e18); assertTrue(_positionManager.isIndexInPosition(tokenId, indexes[0])); assertTrue(_positionManager.isIndexInPosition(tokenId, indexes[1])); assertTrue(_positionManager.isIndexInPosition(tokenId, indexes[2])); // allow position manager to take ownership of the new LPs - _pool.approveLpOwnership(address(_positionManager), indexes[0], 1_000 * 1e27); - _pool.approveLpOwnership(address(_positionManager), indexes[1], 2_000 * 1e27); - _pool.approveLpOwnership(address(_positionManager), indexes[2], 3_000 * 1e27); + _pool.approveLpOwnership(address(_positionManager), indexes[0], 1_000 * 1e18); + _pool.approveLpOwnership(address(_positionManager), indexes[1], 2_000 * 1e18); + _pool.approveLpOwnership(address(_positionManager), indexes[2], 3_000 * 1e18); // rememorialize quote tokens into minted NFT vm.expectEmit(true, true, true, true); emit MemorializePosition(testAddress, tokenId); vm.expectEmit(true, true, true, true); - emit TransferLPTokens(testAddress, address(_positionManager), indexes, 6_000 * 1e27); + emit TransferLPs(testAddress, address(_positionManager), indexes, 6_000 * 1e18); _positionManager.memorializePositions(memorializeParams); // check LP balance - _assertLenderLpBalance( - { - lender: testAddress, - index: indexes[0], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: indexes[0], - lpBalance: 4_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: testAddress, - index: indexes[1], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: indexes[1], - lpBalance: 5_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: testAddress, - index: indexes[2], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: indexes[2], - lpBalance: 6_000 * 1e27, - depositTime: _startTime - } - ); + _assertLenderLpBalance({ + lender: testAddress, + index: indexes[0], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: indexes[0], + lpBalance: 4_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: testAddress, + index: indexes[1], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: indexes[1], + lpBalance: 5_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: testAddress, + index: indexes[2], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: indexes[2], + lpBalance: 6_000 * 1e18, + depositTime: _startTime + }); // check position manager state - assertEq(_positionManager.getLPTokens(tokenId, indexes[0]), 4_000 * 1e27); - assertEq(_positionManager.getLPTokens(tokenId, indexes[1]), 5_000 * 1e27); - assertEq(_positionManager.getLPTokens(tokenId, indexes[2]), 6_000 * 1e27); + assertEq(_positionManager.getLPs(tokenId, indexes[0]), 4_000 * 1e18); + assertEq(_positionManager.getLPs(tokenId, indexes[1]), 5_000 * 1e18); + assertEq(_positionManager.getLPs(tokenId, indexes[2]), 6_000 * 1e18); assertTrue(_positionManager.isIndexInPosition(tokenId, indexes[0])); assertTrue(_positionManager.isIndexInPosition(tokenId, indexes[1])); assertTrue(_positionManager.isIndexInPosition(tokenId, indexes[2])); @@ -525,151 +460,117 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract indexes[2] = 2552; indexes[3] = 2553; - _addInitialLiquidity( - { - from: testLender1, - amount: 3_000 * 1e18, - index: indexes[0] - } - ); - _addInitialLiquidity( - { - from: testLender1, - amount: 3_000 * 1e18, - index: indexes[1] - } - ); - _addInitialLiquidity( - { - from: testLender1, - amount: 3_000 * 1e18, - index: indexes[2] - } - ); - - _addInitialLiquidity( - { - from: testLender2, - amount: 3_000 * 1e18, - index: indexes[0] - } - ); - _addInitialLiquidity( - { - from: testLender2, - amount: 3_000 * 1e18, - index: indexes[3] - } - ); + _addInitialLiquidity({ + from: testLender1, + amount: 3_000 * 1e18, + index: indexes[0] + }); + _addInitialLiquidity({ + from: testLender1, + amount: 3_000 * 1e18, + index: indexes[1] + }); + _addInitialLiquidity({ + from: testLender1, + amount: 3_000 * 1e18, + index: indexes[2] + }); + + _addInitialLiquidity({ + from: testLender2, + amount: 3_000 * 1e18, + index: indexes[0] + }); + _addInitialLiquidity({ + from: testLender2, + amount: 3_000 * 1e18, + index: indexes[3] + }); // mint NFTs to later memorialize existing positions into uint256 tokenId1 = _mintNFT(testLender1, testLender1, address(_pool)); uint256 tokenId2 = _mintNFT(testLender2, testLender2, address(_pool)); // check LPs - _assertLenderLpBalance( - { - lender: testLender1, - index: indexes[0], - lpBalance: 3_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: testLender2, - index: indexes[0], - lpBalance: 3_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: indexes[0], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: testLender1, - index: indexes[1], - lpBalance: 3_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: testLender2, - index: indexes[1], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: indexes[1], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: testLender1, - index: indexes[2], - lpBalance: 3_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: testLender2, - index: indexes[2], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: indexes[2], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: testLender1, - index: indexes[3], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: testLender2, - index: indexes[3], - lpBalance: 3_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: indexes[3], - lpBalance: 0, - depositTime: 0 - } - ); - - assertEq(_positionManager.getLPTokens(indexes[0], tokenId1), 0); - assertEq(_positionManager.getLPTokens(indexes[1], tokenId1), 0); - assertEq(_positionManager.getLPTokens(indexes[2], tokenId1), 0); - - assertEq(_positionManager.getLPTokens(indexes[0], tokenId2), 0); - assertEq(_positionManager.getLPTokens(indexes[3], tokenId2), 0); + _assertLenderLpBalance({ + lender: testLender1, + index: indexes[0], + lpBalance: 3_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: testLender2, + index: indexes[0], + lpBalance: 3_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: indexes[0], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: testLender1, + index: indexes[1], + lpBalance: 3_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: testLender2, + index: indexes[1], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: indexes[1], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: testLender1, + index: indexes[2], + lpBalance: 3_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: testLender2, + index: indexes[2], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: indexes[2], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: testLender1, + index: indexes[3], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: testLender2, + index: indexes[3], + lpBalance: 3_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: indexes[3], + lpBalance: 0, + depositTime: 0 + }); + + assertEq(_positionManager.getLPs(indexes[0], tokenId1), 0); + assertEq(_positionManager.getLPs(indexes[1], tokenId1), 0); + assertEq(_positionManager.getLPs(indexes[2], tokenId1), 0); + + assertEq(_positionManager.getLPs(indexes[0], tokenId2), 0); + assertEq(_positionManager.getLPs(indexes[3], tokenId2), 0); (uint256 poolSize, , , , ) = _poolUtils.poolLoansInfo(address(_pool)); assertEq(poolSize, 15_000 * 1e18); @@ -686,94 +587,78 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract // allow position manager to take ownership of lender 1's position changePrank(testLender1); - _pool.approveLpOwnership(address(_positionManager), indexes[0], 3_000 * 1e27); - _pool.approveLpOwnership(address(_positionManager), indexes[1], 3_000 * 1e27); - _pool.approveLpOwnership(address(_positionManager), indexes[2], 3_000 * 1e27); + _pool.approveLpOwnership(address(_positionManager), indexes[0], 3_000 * 1e18); + _pool.approveLpOwnership(address(_positionManager), indexes[1], 3_000 * 1e18); + _pool.approveLpOwnership(address(_positionManager), indexes[2], 3_000 * 1e18); // memorialize lender 1 quote tokens into minted NFT vm.expectEmit(true, true, true, true); emit MemorializePosition(testLender1, tokenId1); vm.expectEmit(true, true, true, true); - emit TransferLPTokens(testLender1, address(_positionManager), lender1Indexes, 9_000 * 1e27); + emit TransferLPs(testLender1, address(_positionManager), lender1Indexes, 9_000 * 1e18); _positionManager.memorializePositions(memorializeParams); // check lender, position manager, and pool state - _assertLenderLpBalance( - { - lender: testLender1, - index: indexes[0], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: indexes[0], - lpBalance: 3_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: testLender1, - index: indexes[1], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: indexes[1], - lpBalance: 3_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: testLender1, - index: indexes[2], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: indexes[2], - lpBalance: 3_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: testLender1, - index: indexes[3], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: indexes[3], - lpBalance: 0, - depositTime: 0 - } - ); - - assertEq(_positionManager.getLPTokens(tokenId1, indexes[0]), 3_000 * 1e27); - assertEq(_positionManager.getLPTokens(tokenId1, indexes[1]), 3_000 * 1e27); - assertEq(_positionManager.getLPTokens(tokenId1, indexes[2]), 3_000 * 1e27); + _assertLenderLpBalance({ + lender: testLender1, + index: indexes[0], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: indexes[0], + lpBalance: 3_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: testLender1, + index: indexes[1], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: indexes[1], + lpBalance: 3_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: testLender1, + index: indexes[2], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: indexes[2], + lpBalance: 3_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: testLender1, + index: indexes[3], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: indexes[3], + lpBalance: 0, + depositTime: 0 + }); + + assertEq(_positionManager.getLPs(tokenId1, indexes[0]), 3_000 * 1e18); + assertEq(_positionManager.getLPs(tokenId1, indexes[1]), 3_000 * 1e18); + assertEq(_positionManager.getLPs(tokenId1, indexes[2]), 3_000 * 1e18); (poolSize, , , , ) = _poolUtils.poolLoansInfo(address(_pool)); assertEq(poolSize, 15_000 * 1e18); // allow position manager to take ownership of lender 2's position changePrank(testLender2); - _pool.approveLpOwnership(address(_positionManager), indexes[0], 3_000 * 1e27); - _pool.approveLpOwnership(address(_positionManager), indexes[3], 3_000 * 1e27); + _pool.approveLpOwnership(address(_positionManager), indexes[0], 3_000 * 1e18); + _pool.approveLpOwnership(address(_positionManager), indexes[3], 3_000 * 1e18); // memorialize lender 2 quote tokens into minted NFT uint256[] memory newIndexes = new uint256[](2); @@ -787,81 +672,65 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract vm.expectEmit(true, true, true, true); emit MemorializePosition(testLender2, tokenId2); vm.expectEmit(true, true, true, true); - emit TransferLPTokens(testLender2, address(_positionManager), newIndexes, 6_000 * 1e27); + emit TransferLPs(testLender2, address(_positionManager), newIndexes, 6_000 * 1e18); _positionManager.memorializePositions(memorializeParams); // // check lender, position manager, and pool state - _assertLenderLpBalance( - { - lender: testLender2, - index: indexes[0], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: indexes[0], - lpBalance: 6_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: testLender2, - index: indexes[1], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: indexes[1], - lpBalance: 3_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: testLender2, - index: indexes[2], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: indexes[2], - lpBalance: 3_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: testLender2, - index: indexes[3], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: indexes[3], - lpBalance: 3_000 * 1e27, - depositTime: _startTime - } - ); - - assertEq(_positionManager.getLPTokens(tokenId1, indexes[0]), 3_000 * 1e27); - assertEq(_positionManager.getLPTokens(tokenId1, indexes[1]), 3_000 * 1e27); - assertEq(_positionManager.getLPTokens(tokenId1, indexes[2]), 3_000 * 1e27); - - assertEq(_positionManager.getLPTokens(tokenId2, indexes[0]), 3_000 * 1e27); - assertEq(_positionManager.getLPTokens(tokenId2, indexes[3]), 3_000 * 1e27); + _assertLenderLpBalance({ + lender: testLender2, + index: indexes[0], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: indexes[0], + lpBalance: 6_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: testLender2, + index: indexes[1], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: indexes[1], + lpBalance: 3_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: testLender2, + index: indexes[2], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: indexes[2], + lpBalance: 3_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: testLender2, + index: indexes[3], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: indexes[3], + lpBalance: 3_000 * 1e18, + depositTime: _startTime + }); + + assertEq(_positionManager.getLPs(tokenId1, indexes[0]), 3_000 * 1e18); + assertEq(_positionManager.getLPs(tokenId1, indexes[1]), 3_000 * 1e18); + assertEq(_positionManager.getLPs(tokenId1, indexes[2]), 3_000 * 1e18); + + assertEq(_positionManager.getLPs(tokenId2, indexes[0]), 3_000 * 1e18); + assertEq(_positionManager.getLPs(tokenId2, indexes[3]), 3_000 * 1e18); (poolSize, , , , ) = _poolUtils.poolLoansInfo(address(_pool)); assertEq(poolSize, 15_000 * 1e18); @@ -905,86 +774,73 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract // add initial liquidity uint256 mintAmount = 50_000 * 1e18; _mintQuoteAndApproveManagerTokens(testMinter, mintAmount); - _addInitialLiquidity( - { - from: testMinter, - amount: 15_000 * 1e18, - index: testIndexPrice - } - ); + + _addInitialLiquidity({ + from: testMinter, + amount: 15_000 * 1e18, + index: testIndexPrice + }); uint256 tokenId = _mintNFT(testMinter, testMinter, address(_pool)); // check owner assertEq(_positionManager.ownerOf(tokenId), testMinter); // check LPs - _assertLenderLpBalance( - { - lender: testMinter, - index: testIndexPrice, - lpBalance: 15_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: testReceiver, - index: testIndexPrice, - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: testIndexPrice, - lpBalance: 0, - depositTime: 0 - } - ); + _assertLenderLpBalance({ + lender: testMinter, + index: testIndexPrice, + lpBalance: 15_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: testReceiver, + index: testIndexPrice, + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: testIndexPrice, + lpBalance: 0, + depositTime: 0 + }); // check position manager state - assertEq(_positionManager.getLPTokens(tokenId, testIndexPrice), 0); + assertEq(_positionManager.getLPs(tokenId, testIndexPrice), 0); assertFalse(_positionManager.isIndexInPosition(tokenId, testIndexPrice)); // memorialize positions uint256[] memory indexes = new uint256[](1); indexes[0] = testIndexPrice; // allow position manager to take ownership of the position of testMinter - _pool.approveLpOwnership(address(_positionManager), indexes[0], 15_000 * 1e27); + _pool.approveLpOwnership(address(_positionManager), indexes[0], 15_000 * 1e18); // memorialize positions of testMinter IPositionManagerOwnerActions.MemorializePositionsParams memory memorializeParams = IPositionManagerOwnerActions.MemorializePositionsParams( tokenId, indexes ); _positionManager.memorializePositions(memorializeParams); - _assertLenderLpBalance( - { - lender: testMinter, - index: testIndexPrice, - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: testReceiver, - index: testIndexPrice, - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: testIndexPrice, - lpBalance: 15_000 * 1e27, - depositTime: _startTime - } - ); + _assertLenderLpBalance({ + lender: testMinter, + index: testIndexPrice, + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: testReceiver, + index: testIndexPrice, + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: testIndexPrice, + lpBalance: 15_000 * 1e18, + depositTime: _startTime + }); // check position manager state - assertEq(_positionManager.getLPTokens(tokenId, testIndexPrice), 15_000 * 1e27); + assertEq(_positionManager.getLPs(tokenId, testIndexPrice), 15_000 * 1e18); assertTrue(_positionManager.isIndexInPosition(tokenId, testIndexPrice)); // approve and transfer NFT to different address @@ -1008,33 +864,27 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract _positionManager.reedemPositions(reedemParams); // check pool state - _assertLenderLpBalance( - { - lender: testMinter, - index: testIndexPrice, - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: testReceiver, - index: testIndexPrice, - lpBalance: 15_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: testIndexPrice, - lpBalance: 0, - depositTime: 0 - } - ); + _assertLenderLpBalance({ + lender: testMinter, + index: testIndexPrice, + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: testReceiver, + index: testIndexPrice, + lpBalance: 15_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: testIndexPrice, + lpBalance: 0, + depositTime: 0 + }); // check position manager state - assertEq(_positionManager.getLPTokens(tokenId, testIndexPrice), 0); + assertEq(_positionManager.getLPs(tokenId, testIndexPrice), 0); assertFalse(_positionManager.isIndexInPosition(tokenId, testIndexPrice)); } @@ -1053,53 +903,46 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract // add initial liquidity uint256 mintAmount = 50_000 * 1e18; _mintQuoteAndApproveManagerTokens(testMinter, mintAmount); - _addInitialLiquidity( - { - from: testMinter, - amount: 15_000 * 1e18, - index: testIndexPrice - } - ); + + _addInitialLiquidity({ + from: testMinter, + amount: 15_000 * 1e18, + index: testIndexPrice + }); uint256 tokenId = _mintNFT(testMinter, testMinter, address(_pool)); // check owner assertEq(_positionManager.ownerOf(tokenId), testMinter); // check LPs - _assertLenderLpBalance( - { - lender: testMinter, - index: testIndexPrice, - lpBalance: 15_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: testReceiver, - index: testIndexPrice, - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: testIndexPrice, - lpBalance: 0, - depositTime: 0 - } - ); + _assertLenderLpBalance({ + lender: testMinter, + index: testIndexPrice, + lpBalance: 15_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: testReceiver, + index: testIndexPrice, + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: testIndexPrice, + lpBalance: 0, + depositTime: 0 + }); // check position manager state - assertEq(_positionManager.getLPTokens(tokenId, testIndexPrice), 0); + assertEq(_positionManager.getLPs(tokenId, testIndexPrice), 0); assertFalse(_positionManager.isIndexInPosition(tokenId, testIndexPrice)); // memorialize positions uint256[] memory indexes = new uint256[](1); indexes[0] = testIndexPrice; // allow position manager to take ownership of the position of testMinter - _pool.approveLpOwnership(address(_positionManager), indexes[0], 15_000 * 1e27); + _pool.approveLpOwnership(address(_positionManager), indexes[0], 15_000 * 1e18); // memorialize positions of testMinter IPositionManagerOwnerActions.MemorializePositionsParams memory memorializeParams = IPositionManagerOwnerActions.MemorializePositionsParams( tokenId, indexes @@ -1107,35 +950,31 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract _positionManager.memorializePositions(memorializeParams); // check pool state - _assertLenderLpBalance( - { - lender: testMinter, - index: testIndexPrice, - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: testReceiver, - index: testIndexPrice, - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: testIndexPrice, - lpBalance: 15_000 * 1e27, - depositTime: _startTime - } - ); + _assertLenderLpBalance({ + lender: testMinter, + index: testIndexPrice, + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: testReceiver, + index: testIndexPrice, + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: testIndexPrice, + lpBalance: 15_000 * 1e18, + depositTime: _startTime + }); // check position manager state - assertEq(_positionManager.getLPTokens(tokenId, testIndexPrice), 15_000 * 1e27); + assertEq(_positionManager.getLPs(tokenId, testIndexPrice), 15_000 * 1e18); assertTrue(_positionManager.isIndexInPosition(tokenId, testIndexPrice)); + address testSpender = makeAddr("testSpender"); + // approve and transfer NFT by permit to different address { uint256 deadline = block.timestamp + 1 days; @@ -1148,7 +987,7 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract keccak256( abi.encode( _positionManager.PERMIT_TYPEHASH(), - testReceiver, + testSpender, tokenId, 0, deadline @@ -1157,7 +996,8 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract ) ) ); - _positionManager.safeTransferFromWithPermit(testMinter, testReceiver, testReceiver, tokenId, deadline, v, r, s ); + changePrank(testSpender); + _positionManager.safeTransferFromWithPermit(testMinter, testReceiver, tokenId, deadline, v, r, s ); } // check owner @@ -1177,37 +1017,33 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract _positionManager.reedemPositions(reedemParams); // check pool state - _assertLenderLpBalance( - { - lender: testMinter, - index: testIndexPrice, - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: testReceiver, - index: testIndexPrice, - lpBalance: 15_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: testIndexPrice, - lpBalance: 0, - depositTime: 0 - } - ); + _assertLenderLpBalance({ + lender: testMinter, + index: testIndexPrice, + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: testReceiver, + index: testIndexPrice, + lpBalance: 15_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: testIndexPrice, + lpBalance: 0, + depositTime: 0 + }); // check position manager state - assertEq(_positionManager.getLPTokens(tokenId, testIndexPrice), 0); + assertEq(_positionManager.getLPs(tokenId, testIndexPrice), 0); assertFalse(_positionManager.isIndexInPosition(tokenId, testIndexPrice)); } function testPermitByContract() external { + address testSpender = makeAddr("spender"); + // deploy recipient contract (address nonMintingContractOwner, uint256 nonMintingContractPrivateKey) = makeAddrAndKey("nonMintingContract"); ContractNFTRecipient recipientContract = new ContractNFTRecipient(nonMintingContractOwner); @@ -1217,50 +1053,58 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract ContractNFTRecipient ownerContract = new ContractNFTRecipient(testContractOwner); uint256 tokenId = _mintNFT(address(ownerContract), address(ownerContract), address(_pool)); + changePrank(testSpender); + // check contract owned nft can't be signed by non owner uint256 deadline = block.timestamp + 1 days; - (uint8 v, bytes32 r, bytes32 s) = _getPermitSig(address(recipientContract), tokenId, deadline, nonMintingContractPrivateKey); + (uint8 v, bytes32 r, bytes32 s) = _getPermitSig(testSpender, tokenId, deadline, nonMintingContractPrivateKey); vm.expectRevert("ajna/nft-unauthorized"); - _positionManager.safeTransferFromWithPermit(address(ownerContract), address(recipientContract), address(recipientContract), tokenId, deadline, v, r, s ); + _positionManager.safeTransferFromWithPermit(address(ownerContract), address(recipientContract), tokenId, deadline, v, r, s ); // check owner can permit their contract to transfer the NFT deadline = block.timestamp + 1 days; - (v, r, s) = _getPermitSig(address(recipientContract), tokenId, deadline, ownerPrivateKey); - _positionManager.safeTransferFromWithPermit(address(ownerContract), address(recipientContract), address(recipientContract), tokenId, deadline, v, r, s ); + (v, r, s) = _getPermitSig(testSpender, tokenId, deadline, ownerPrivateKey); + _positionManager.safeTransferFromWithPermit(address(ownerContract), address(recipientContract), tokenId, deadline, v, r, s ); } function testPermitReverts() external { // generate addresses and set test params (address testMinter, uint256 minterPrivateKey) = makeAddrAndKey("testMinter"); (address testReceiver, uint256 receiverPrivateKey) = makeAddrAndKey("testReceiver"); + address testSpender = makeAddr("spender"); vm.prank(testMinter); uint256 tokenId = _mintNFT(testMinter, testMinter, address(_pool)); assertEq(_positionManager.ownerOf(tokenId), testMinter); + changePrank(testSpender); + // check can't use a deadline in the past uint256 deadline = block.timestamp - 1 days; - (uint8 v, bytes32 r, bytes32 s) = _getPermitSig(testReceiver, tokenId, deadline, minterPrivateKey); + (uint8 v, bytes32 r, bytes32 s) = _getPermitSig(testSpender, tokenId, deadline, minterPrivateKey); vm.expectRevert("ajna/nft-permit-expired"); - _positionManager.safeTransferFromWithPermit(testMinter, testReceiver, testReceiver, tokenId, deadline, v, r, s ); + _positionManager.safeTransferFromWithPermit(testMinter, testReceiver, tokenId, deadline, v, r, s ); // check can't self approve + changePrank(testMinter); deadline = block.timestamp + 1 days; - (v, r, s) = _getPermitSig(testMinter, tokenId, deadline, minterPrivateKey); + (v, r, s) = _getPermitSig(testSpender, tokenId, deadline, minterPrivateKey); vm.expectRevert("ERC721Permit: approval to current owner"); - _positionManager.safeTransferFromWithPermit(testMinter, testMinter, testMinter, tokenId, deadline, v, r, s ); + _positionManager.safeTransferFromWithPermit(testMinter, testMinter, tokenId, deadline, v, r, s ); + + changePrank(testSpender); // check signer is authorized to permit deadline = block.timestamp + 1 days; - (v, r, s) = _getPermitSig(testReceiver, tokenId, deadline, receiverPrivateKey); + (v, r, s) = _getPermitSig(testSpender, tokenId, deadline, receiverPrivateKey); vm.expectRevert("ajna/nft-unauthorized"); - _positionManager.safeTransferFromWithPermit(testMinter, testReceiver, testReceiver, tokenId, deadline, v, r, s ); + _positionManager.safeTransferFromWithPermit(testMinter, testReceiver, tokenId, deadline, v, r, s ); // check signature is valid deadline = block.timestamp + 1 days; - (v, r, s) = _getPermitSig(testReceiver, tokenId, deadline, minterPrivateKey); + (v, r, s) = _getPermitSig(testSpender, tokenId, deadline, minterPrivateKey); vm.expectRevert("ajna/nft-invalid-signature"); - _positionManager.safeTransferFromWithPermit(testMinter, testReceiver, testReceiver, tokenId, deadline, 0, r, s ); + _positionManager.safeTransferFromWithPermit(testMinter, testReceiver, tokenId, deadline, 0, r, s ); } /** @@ -1299,13 +1143,12 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract // add initial liquidity uint256 mintAmount = 50_000 * 1e18; _mintQuoteAndApproveManagerTokens(testMinter, mintAmount); - _addInitialLiquidity( - { - from: testMinter, - amount: 15_000 * 1e18, - index: testIndexPrice - } - ); + + _addInitialLiquidity({ + from: testMinter, + amount: 15_000 * 1e18, + index: testIndexPrice + }); uint256 tokenId = _mintNFT(testMinter, testMinter, address(_pool)); @@ -1316,7 +1159,7 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract uint256[] memory indexes = new uint256[](1); indexes[0] = testIndexPrice; // allow position manager to take ownership of the position of testMinter - _pool.approveLpOwnership(address(_positionManager), indexes[0], 15_000 * 1e27); + _pool.approveLpOwnership(address(_positionManager), indexes[0], 15_000 * 1e18); // memorialize positions of testMinter IPositionManagerOwnerActions.MemorializePositionsParams memory memorializeParams = IPositionManagerOwnerActions.MemorializePositionsParams( tokenId, indexes @@ -1353,20 +1196,18 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract address notOwner = makeAddr("notOwner"); _mintQuoteAndApproveManagerTokens(testAddress, 10_000 * 1e18); - _addInitialLiquidity( - { - from: testAddress, - amount: 10_000 * 1e18, - index: 2550 - } - ); + _addInitialLiquidity({ + from: testAddress, + amount: 10_000 * 1e18, + index: 2550 + }); // mint position NFT uint256 tokenId = _mintNFT(testAddress, testAddress, address(_pool)); // construct move liquidity params IPositionManagerOwnerActions.MoveLiquidityParams memory moveLiquidityParams = IPositionManagerOwnerActions.MoveLiquidityParams( - tokenId, address(_pool), 2550, 2551 + tokenId, address(_pool), 2550, 2551, block.timestamp + 30 ); // move liquidity should fail because is not performed by owner @@ -1386,20 +1227,16 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract _mintQuoteAndApproveManagerTokens(testAddress2, 10_000 * 1e18); _mintCollateralAndApproveTokens(testAddress3, 10_000 * 1e18); - _addInitialLiquidity( - { - from: testAddress1, - amount: 2_500 * 1e18, - index: mintIndex - } - ); - _addInitialLiquidity( - { - from: testAddress2, - amount: 5_500 * 1e18, - index: mintIndex - } - ); + _addInitialLiquidity({ + from: testAddress1, + amount: 2_500 * 1e18, + index: mintIndex + }); + _addInitialLiquidity({ + from: testAddress2, + amount: 5_500 * 1e18, + index: mintIndex + }); uint256 tokenId1 = _mintNFT(testAddress1, testAddress1, address(_pool)); uint256 tokenId2 = _mintNFT(testAddress2, testAddress2, address(_pool)); @@ -1407,60 +1244,48 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract assertEq(_positionManager.ownerOf(tokenId2), testAddress2); // check pool state - _assertLenderLpBalance( - { - lender: testAddress1, - index: mintIndex, - lpBalance: 2_500 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: testAddress2, - index: mintIndex, - lpBalance: 5_500 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: mintIndex, - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: testAddress1, - index: moveIndex, - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: testAddress2, - index: moveIndex, - lpBalance: 0 * 1e27, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: moveIndex, - lpBalance: 0, - depositTime: 0 - } - ); + _assertLenderLpBalance({ + lender: testAddress1, + index: mintIndex, + lpBalance: 2_500 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: testAddress2, + index: mintIndex, + lpBalance: 5_500 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: mintIndex, + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: testAddress1, + index: moveIndex, + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: testAddress2, + index: moveIndex, + lpBalance: 0 * 1e18, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: moveIndex, + lpBalance: 0, + depositTime: 0 + }); // check position manager state - assertEq(_positionManager.getLPTokens(tokenId1, mintIndex), 0); - assertEq(_positionManager.getLPTokens(tokenId1, moveIndex), 0); - assertEq(_positionManager.getLPTokens(tokenId2, mintIndex), 0); - assertEq(_positionManager.getLPTokens(tokenId2, moveIndex), 0); + assertEq(_positionManager.getLPs(tokenId1, mintIndex), 0); + assertEq(_positionManager.getLPs(tokenId1, moveIndex), 0); + assertEq(_positionManager.getLPs(tokenId2, mintIndex), 0); + assertEq(_positionManager.getLPs(tokenId2, moveIndex), 0); assertFalse(_positionManager.isIndexInPosition(tokenId1, mintIndex)); assertFalse(_positionManager.isIndexInPosition(tokenId1, moveIndex)); assertFalse(_positionManager.isIndexInPosition(tokenId2, mintIndex)); @@ -1468,7 +1293,7 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract // allow position manager to take ownership of the position of testAddress1 changePrank(testAddress1); - _pool.approveLpOwnership(address(_positionManager), mintIndex, 2_500 * 1e27); + _pool.approveLpOwnership(address(_positionManager), mintIndex, 2_500 * 1e18); // memorialize positions of testAddress1 uint256[] memory indexes = new uint256[](1); @@ -1480,60 +1305,48 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract _positionManager.memorializePositions(memorializeParams); // check pool state - _assertLenderLpBalance( - { - lender: testAddress1, - index: mintIndex, - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: testAddress2, - index: mintIndex, - lpBalance: 5_500 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: mintIndex, - lpBalance: 2_500 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: testAddress1, - index: moveIndex, - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: testAddress2, - index: moveIndex, - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: moveIndex, - lpBalance: 0, - depositTime: 0 - } - ); + _assertLenderLpBalance({ + lender: testAddress1, + index: mintIndex, + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: testAddress2, + index: mintIndex, + lpBalance: 5_500 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: mintIndex, + lpBalance: 2_500 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: testAddress1, + index: moveIndex, + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: testAddress2, + index: moveIndex, + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: moveIndex, + lpBalance: 0, + depositTime: 0 + }); // check position manager state - assertEq(_positionManager.getLPTokens(tokenId1, mintIndex), 2_500 * 1e27); - assertEq(_positionManager.getLPTokens(tokenId1, moveIndex), 0); - assertEq(_positionManager.getLPTokens(tokenId2, mintIndex), 0); - assertEq(_positionManager.getLPTokens(tokenId2, moveIndex), 0); + assertEq(_positionManager.getLPs(tokenId1, mintIndex), 2_500 * 1e18); + assertEq(_positionManager.getLPs(tokenId1, moveIndex), 0); + assertEq(_positionManager.getLPs(tokenId2, mintIndex), 0); + assertEq(_positionManager.getLPs(tokenId2, moveIndex), 0); assertTrue(_positionManager.isIndexInPosition(tokenId1, mintIndex)); assertFalse(_positionManager.isIndexInPosition(tokenId1, moveIndex)); assertFalse(_positionManager.isIndexInPosition(tokenId2, mintIndex)); @@ -1541,7 +1354,7 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract // construct move liquidity params IPositionManagerOwnerActions.MoveLiquidityParams memory moveLiquidityParams = IPositionManagerOwnerActions.MoveLiquidityParams( - tokenId1, address(_pool), mintIndex, moveIndex + tokenId1, address(_pool), mintIndex, moveIndex, block.timestamp + 30 ); // move liquidity called by testAddress1 owner @@ -1551,60 +1364,48 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract _positionManager.moveLiquidity(moveLiquidityParams); // check pool state - _assertLenderLpBalance( - { - lender: testAddress1, - index: mintIndex, - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: testAddress2, - index: mintIndex, - lpBalance: 5_500 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: mintIndex, - lpBalance: 0, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: testAddress1, - index: moveIndex, - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: testAddress2, - index: moveIndex, - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: moveIndex, - lpBalance: 2_500 * 1e27, - depositTime: _startTime - } - ); + _assertLenderLpBalance({ + lender: testAddress1, + index: mintIndex, + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: testAddress2, + index: mintIndex, + lpBalance: 5_500 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: mintIndex, + lpBalance: 0, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: testAddress1, + index: moveIndex, + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: testAddress2, + index: moveIndex, + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: moveIndex, + lpBalance: 2_500 * 1e18, + depositTime: _startTime + }); // check position manager state - assertEq(_positionManager.getLPTokens(tokenId1, mintIndex), 0); - assertEq(_positionManager.getLPTokens(tokenId1, moveIndex), 2_500 * 1e27); - assertEq(_positionManager.getLPTokens(tokenId2, mintIndex), 0); - assertEq(_positionManager.getLPTokens(tokenId2, moveIndex), 0); + assertEq(_positionManager.getLPs(tokenId1, mintIndex), 0); + assertEq(_positionManager.getLPs(tokenId1, moveIndex), 2_500 * 1e18); + assertEq(_positionManager.getLPs(tokenId2, mintIndex), 0); + assertEq(_positionManager.getLPs(tokenId2, moveIndex), 0); assertFalse(_positionManager.isIndexInPosition(tokenId1, mintIndex)); assertTrue(_positionManager.isIndexInPosition(tokenId1, moveIndex)); assertFalse(_positionManager.isIndexInPosition(tokenId2, mintIndex)); @@ -1612,7 +1413,7 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract // allow position manager to take ownership of the position of testAddress2 changePrank(testAddress2); - _pool.approveLpOwnership(address(_positionManager), mintIndex, 5_500 * 1e27); + _pool.approveLpOwnership(address(_positionManager), mintIndex, 5_500 * 1e18); // memorialize positions of testAddress2 memorializeParams = IPositionManagerOwnerActions.MemorializePositionsParams( @@ -1622,60 +1423,48 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract _positionManager.memorializePositions(memorializeParams); // check pool state - _assertLenderLpBalance( - { - lender: testAddress1, - index: mintIndex, - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: testAddress2, - index: mintIndex, - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: mintIndex, - lpBalance: 5_500 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: testAddress1, - index: moveIndex, - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: testAddress2, - index: moveIndex, - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: moveIndex, - lpBalance: 2_500 * 1e27, - depositTime: _startTime - } - ); + _assertLenderLpBalance({ + lender: testAddress1, + index: mintIndex, + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: testAddress2, + index: mintIndex, + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: mintIndex, + lpBalance: 5_500 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: testAddress1, + index: moveIndex, + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: testAddress2, + index: moveIndex, + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: moveIndex, + lpBalance: 2_500 * 1e18, + depositTime: _startTime + }); // check position manager state - assertEq(_positionManager.getLPTokens(tokenId1, mintIndex), 0); - assertEq(_positionManager.getLPTokens(tokenId1, moveIndex), 2_500 * 1e27); - assertEq(_positionManager.getLPTokens(tokenId2, mintIndex), 5_500 * 1e27); - assertEq(_positionManager.getLPTokens(tokenId2, moveIndex), 0); + assertEq(_positionManager.getLPs(tokenId1, mintIndex), 0); + assertEq(_positionManager.getLPs(tokenId1, moveIndex), 2_500 * 1e18); + assertEq(_positionManager.getLPs(tokenId2, mintIndex), 5_500 * 1e18); + assertEq(_positionManager.getLPs(tokenId2, moveIndex), 0); assertFalse(_positionManager.isIndexInPosition(tokenId1, mintIndex)); assertTrue(_positionManager.isIndexInPosition(tokenId1, moveIndex)); assertTrue(_positionManager.isIndexInPosition(tokenId2, mintIndex)); @@ -1683,17 +1472,15 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract // construct move liquidity params moveLiquidityParams = IPositionManagerOwnerActions.MoveLiquidityParams( - tokenId2, address(_pool), mintIndex, moveIndex + tokenId2, address(_pool), mintIndex, moveIndex, block.timestamp + 30 ); - _addCollateral( - { - from: testAddress3, - amount: 10_000 * 1e18, - index: mintIndex, - lpAward: 30_108_920.22197881557845 * 1e27 - } - ); + _addCollateral({ + from: testAddress3, + amount: 10_000 * 1e18, + index: mintIndex, + lpAward: 30_108_920.22197881557845 * 1e18 + }); // move liquidity called by testAddress2 owner vm.expectEmit(true, true, true, true); @@ -1702,60 +1489,48 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract _positionManager.moveLiquidity(moveLiquidityParams); // check pool state - _assertLenderLpBalance( - { - lender: testAddress1, - index: mintIndex, - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: testAddress2, - index: mintIndex, - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: mintIndex, - lpBalance: 0 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: testAddress1, - index: moveIndex, - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: testAddress2, - index: moveIndex, - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: moveIndex, - lpBalance: 8_000 * 1e27, - depositTime: _startTime - } - ); + _assertLenderLpBalance({ + lender: testAddress1, + index: mintIndex, + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: testAddress2, + index: mintIndex, + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: mintIndex, + lpBalance: 0 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: testAddress1, + index: moveIndex, + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: testAddress2, + index: moveIndex, + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: moveIndex, + lpBalance: 8_000 * 1e18, + depositTime: _startTime + }); // check position manager state - assertEq(_positionManager.getLPTokens(tokenId1, mintIndex), 0); - assertEq(_positionManager.getLPTokens(tokenId1, moveIndex), 2_500 * 1e27); - assertEq(_positionManager.getLPTokens(tokenId2, mintIndex), 0); - assertEq(_positionManager.getLPTokens(tokenId2, moveIndex), 5_500 * 1e27); + assertEq(_positionManager.getLPs(tokenId1, mintIndex), 0); + assertEq(_positionManager.getLPs(tokenId1, moveIndex), 2_500 * 1e18); + assertEq(_positionManager.getLPs(tokenId2, mintIndex), 0); + assertEq(_positionManager.getLPs(tokenId2, moveIndex), 5_500 * 1e18); assertFalse(_positionManager.isIndexInPosition(tokenId1, mintIndex)); assertTrue(_positionManager.isIndexInPosition(tokenId1, moveIndex)); assertFalse(_positionManager.isIndexInPosition(tokenId2, mintIndex)); @@ -1763,7 +1538,7 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract // check can't move liquidity from position with no liquidity moveLiquidityParams = IPositionManagerOwnerActions.MoveLiquidityParams( - tokenId2, address(_pool), 1000, 2000 + tokenId2, address(_pool), 1000, 2000, block.timestamp + 30 ); changePrank(address(testAddress2)); vm.expectRevert(IPositionManagerErrors.RemoveLiquidityFailed.selector); @@ -1778,45 +1553,40 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract // add initial liquidity uint256 mintAmount = 50_000 * 1e18; _mintQuoteAndApproveManagerTokens(testMinter, mintAmount); - _addInitialLiquidity( - { - from: testMinter, - amount: 15_000 * 1e18, - index: testIndexPrice - } - ); + + _addInitialLiquidity({ + from: testMinter, + amount: 15_000 * 1e18, + index: testIndexPrice + }); uint256 tokenId = _mintNFT(testMinter, testMinter, address(_pool)); // check owner assertEq(_positionManager.ownerOf(tokenId), testMinter); // check pool state - _assertLenderLpBalance( - { - lender: testMinter, - index: testIndexPrice, - lpBalance: 15_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: testIndexPrice, - lpBalance: 0, - depositTime: 0 - } - ); + _assertLenderLpBalance({ + lender: testMinter, + index: testIndexPrice, + lpBalance: 15_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: testIndexPrice, + lpBalance: 0, + depositTime: 0 + }); // check position manager state - assertEq(_positionManager.getLPTokens(tokenId, testIndexPrice), 0); + assertEq(_positionManager.getLPs(tokenId, testIndexPrice), 0); assertFalse(_positionManager.isIndexInPosition(tokenId, testIndexPrice)); // memorialize positions uint256[] memory indexes = new uint256[](1); indexes[0] = testIndexPrice; // allow position manager to take ownership of the position of testMinter - _pool.approveLpOwnership(address(_positionManager), indexes[0], 15_000 * 1e27); + _pool.approveLpOwnership(address(_positionManager), indexes[0], 15_000 * 1e18); // memorialize positions of testMinter IPositionManagerOwnerActions.MemorializePositionsParams memory memorializeParams = IPositionManagerOwnerActions.MemorializePositionsParams( tokenId, indexes @@ -1824,25 +1594,21 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract _positionManager.memorializePositions(memorializeParams); // check pool state - _assertLenderLpBalance( - { - lender: testMinter, - index: testIndexPrice, - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: testIndexPrice, - lpBalance: 15_000 * 1e27, - depositTime: _startTime - } - ); + _assertLenderLpBalance({ + lender: testMinter, + index: testIndexPrice, + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: testIndexPrice, + lpBalance: 15_000 * 1e18, + depositTime: _startTime + }); // check position manager state - assertEq(_positionManager.getLPTokens(tokenId, testIndexPrice), 15_000 * 1e27); + assertEq(_positionManager.getLPs(tokenId, testIndexPrice), 15_000 * 1e18); assertTrue(_positionManager.isIndexInPosition(tokenId, testIndexPrice)); // redeem positions of testMinter @@ -1862,25 +1628,21 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract _positionManager.reedemPositions(reedemParams); // check pool state - _assertLenderLpBalance( - { - lender: testMinter, - index: testIndexPrice, - lpBalance: 15_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: testIndexPrice, - lpBalance: 0, - depositTime: 0 - } - ); + _assertLenderLpBalance({ + lender: testMinter, + index: testIndexPrice, + lpBalance: 15_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: testIndexPrice, + lpBalance: 0, + depositTime: 0 + }); // check position manager state - assertEq(_positionManager.getLPTokens(tokenId, testIndexPrice), 0); + assertEq(_positionManager.getLPs(tokenId, testIndexPrice), 0); assertFalse(_positionManager.isIndexInPosition(tokenId, testIndexPrice)); // should fail if trying to redeem one more time @@ -1916,92 +1678,75 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract uint256 mintAmount = 50_000 * 1e18; _mintQuoteAndApproveManagerTokens(testMinter, mintAmount); _mintQuoteAndApproveManagerTokens(testReceiver, mintAmount); - _addInitialLiquidity( - { - from: testReceiver, - amount: 25_000 * 1e18, - index: testIndexPrice - } - ); - _addInitialLiquidity( - { - from: testReceiver, - amount: 15_000 * 1e18, - index: 2551 - } - ); - _addInitialLiquidity( - { - from: testMinter, - amount: 15_000 * 1e18, - index: testIndexPrice - } - ); + _addInitialLiquidity({ + from: testReceiver, + amount: 25_000 * 1e18, + index: testIndexPrice + }); + _addInitialLiquidity({ + from: testReceiver, + amount: 15_000 * 1e18, + index: 2551 + }); + + _addInitialLiquidity({ + from: testMinter, + amount: 15_000 * 1e18, + index: testIndexPrice + }); uint256 tokenId = _mintNFT(testMinter, testMinter, address(_pool)); // check owner assertEq(_positionManager.ownerOf(tokenId), testMinter); // check pool state - _assertLenderLpBalance( - { - lender: testMinter, - index: testIndexPrice, - lpBalance: 15_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: testReceiver, - index: testIndexPrice, - lpBalance: 25_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: testIndexPrice, - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: testMinter, - index: 2551, - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: testReceiver, - index: 2551, - lpBalance: 15_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: 2551, - lpBalance: 0, - depositTime: 0 - } - ); + _assertLenderLpBalance({ + lender: testMinter, + index: testIndexPrice, + lpBalance: 15_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: testReceiver, + index: testIndexPrice, + lpBalance: 25_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: testIndexPrice, + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: testMinter, + index: 2551, + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: testReceiver, + index: 2551, + lpBalance: 15_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: 2551, + lpBalance: 0, + depositTime: 0 + }); // check position manager state - assertEq(_positionManager.getLPTokens(tokenId, testIndexPrice), 0); + assertEq(_positionManager.getLPs(tokenId, testIndexPrice), 0); assertFalse(_positionManager.isIndexInPosition(tokenId, testIndexPrice)); // memorialize positions uint256[] memory indexes = new uint256[](1); indexes[0] = testIndexPrice; // allow position manager to take ownership of the position of testMinter - _pool.approveLpOwnership(address(_positionManager), indexes[0], 15_000 * 1e27); + _pool.approveLpOwnership(address(_positionManager), indexes[0], 15_000 * 1e18); // memorialize positions of testMinter IPositionManagerOwnerActions.MemorializePositionsParams memory memorializeParams = IPositionManagerOwnerActions.MemorializePositionsParams( tokenId, indexes @@ -2009,57 +1754,45 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract _positionManager.memorializePositions(memorializeParams); // check pool state - _assertLenderLpBalance( - { - lender: testMinter, - index: testIndexPrice, - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: testReceiver, - index: testIndexPrice, - lpBalance: 25_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: testIndexPrice, - lpBalance: 15_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: testMinter, - index: 2551, - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: testReceiver, - index: 2551, - lpBalance: 15_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: 2551, - lpBalance: 0, - depositTime: 0 - } - ); + _assertLenderLpBalance({ + lender: testMinter, + index: testIndexPrice, + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: testReceiver, + index: testIndexPrice, + lpBalance: 25_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: testIndexPrice, + lpBalance: 15_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: testMinter, + index: 2551, + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: testReceiver, + index: 2551, + lpBalance: 15_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: 2551, + lpBalance: 0, + depositTime: 0 + }); // check position manager state - assertEq(_positionManager.getLPTokens(tokenId, testIndexPrice), 15_000 * 1e27); + assertEq(_positionManager.getLPs(tokenId, testIndexPrice), 15_000 * 1e18); assertTrue(_positionManager.isIndexInPosition(tokenId, testIndexPrice)); // approve and transfer NFT to different address @@ -2089,62 +1822,50 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract vm.expectEmit(true, true, true, true); emit RedeemPosition(testReceiver, tokenId); vm.expectEmit(true, true, true, true); - emit TransferLPTokens(address(_positionManager), testReceiver, indexes, 15_000 * 1e27); + emit TransferLPs(address(_positionManager), testReceiver, indexes, 15_000 * 1e18); changePrank(testReceiver); _positionManager.reedemPositions(reedemParams); // check pool state - _assertLenderLpBalance( - { - lender: testMinter, - index: testIndexPrice, - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: testReceiver, - index: testIndexPrice, - lpBalance: 40_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: testIndexPrice, - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: testMinter, - index: 2551, - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: testReceiver, - index: 2551, - lpBalance: 15_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: 2551, - lpBalance: 0, - depositTime: 0 - } - ); + _assertLenderLpBalance({ + lender: testMinter, + index: testIndexPrice, + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: testReceiver, + index: testIndexPrice, + lpBalance: 40_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: testIndexPrice, + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: testMinter, + index: 2551, + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: testReceiver, + index: 2551, + lpBalance: 15_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: 2551, + lpBalance: 0, + depositTime: 0 + }); // check position manager state - assertEq(_positionManager.getLPTokens(tokenId, testIndexPrice), 0); + assertEq(_positionManager.getLPs(tokenId, testIndexPrice), 0); assertFalse(_positionManager.isIndexInPosition(tokenId, testIndexPrice)); } @@ -2159,39 +1880,32 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract uint256[] memory indexes = new uint256[](1); indexes[0] = 2550; - _addInitialLiquidity( - { - from: lender, - amount: 10_000 * 1e18, - index: 2550 - } - ); - _assertLenderLpBalance( - { - lender: lender, - index: 2550, - lpBalance: 10_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: minter, - index: 2550, - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: 2550, - lpBalance: 0, - depositTime: 0 - } - ); + _addInitialLiquidity({ + from: lender, + amount: 10_000 * 1e18, + index: 2550 + }); + _assertLenderLpBalance({ + lender: lender, + index: 2550, + lpBalance: 10_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: minter, + index: 2550, + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: 2550, + lpBalance: 0, + depositTime: 0 + }); + // allow position manager to take ownership of the position - _pool.approveLpOwnership(address(_positionManager), indexes[0], 10_000 * 1e27); + _pool.approveLpOwnership(address(_positionManager), indexes[0], 10_000 * 1e18); // 3rd party minter mints NFT and memorialize lender positions uint256 tokenId = _mintNFT(minter, lender, address(_pool)); @@ -2200,34 +1914,29 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract tokenId, indexes ); _positionManager.memorializePositions(memorializeParams); - _assertLenderLpBalance( - { - lender: lender, - index: 2550, - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: minter, - index: 2550, - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: 2550, - lpBalance: 10_000 * 1e27, - depositTime: _startTime - } - ); + + _assertLenderLpBalance({ + lender: lender, + index: 2550, + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: minter, + index: 2550, + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: 2550, + lpBalance: 10_000 * 1e18, + depositTime: _startTime + }); // minter cannot move liquidity on behalf of lender (is not approved) IPositionManagerOwnerActions.MoveLiquidityParams memory moveLiquidityParams = IPositionManagerOwnerActions.MoveLiquidityParams( - tokenId, address(_pool), 2550, 2551 + tokenId, address(_pool), 2550, 2551, block.timestamp + 30 ); vm.expectRevert(IPositionManagerErrors.NoAuth.selector); _positionManager.moveLiquidity(moveLiquidityParams); @@ -2253,30 +1962,25 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract changePrank(minter); // minter can move liquidity on behalf of lender _positionManager.moveLiquidity(moveLiquidityParams); - _assertLenderLpBalance( - { - lender: lender, - index: 2551, - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: minter, - index: 2551, - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: 2551, - lpBalance: 10_000 * 1e27, - depositTime: _startTime - } - ); + + _assertLenderLpBalance({ + lender: lender, + index: 2551, + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: minter, + index: 2551, + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: 2551, + lpBalance: 10_000 * 1e18, + depositTime: _startTime + }); // minter can redeem liquidity on behalf of lender indexes[0] = 2551; @@ -2284,30 +1988,25 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract tokenId, address(_pool), indexes ); _positionManager.reedemPositions(reedemParams); - _assertLenderLpBalance( - { - lender: lender, - index: 2551, - lpBalance: 10_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: minter, - index: 2551, - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: 2551, - lpBalance: 0, - depositTime: 0 - } - ); + + _assertLenderLpBalance({ + lender: lender, + index: 2551, + lpBalance: 10_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: minter, + index: 2551, + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: 2551, + lpBalance: 0, + depositTime: 0 + }); // minter can burn NFT on behalf of lender _positionManager.burn(burnParams); @@ -2326,31 +2025,26 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract uint256[] memory indexes = new uint256[](1); indexes[0] = 2550; - _addInitialLiquidity( - { - from: lender, - amount: 10_000 * 1e18, - index: 2550 - } - ); - _assertLenderLpBalance( - { - lender: lender, - index: 2550, - lpBalance: 10_000 * 1e27, - depositTime: _startTime - } - ); - _assertLenderLpBalance( - { - lender: minter, - index: 2550, - lpBalance: 0, - depositTime: 0 - } - ); + _addInitialLiquidity({ + from: lender, + amount: 10_000 * 1e18, + index: 2550 + }); + _assertLenderLpBalance({ + lender: lender, + index: 2550, + lpBalance: 10_000 * 1e18, + depositTime: _startTime + }); + _assertLenderLpBalance({ + lender: minter, + index: 2550, + lpBalance: 0, + depositTime: 0 + }); + // allow position manager to take ownership of the position - _pool.approveLpOwnership(address(_positionManager), indexes[0], 10_000 * 1e27); + _pool.approveLpOwnership(address(_positionManager), indexes[0], 10_000 * 1e18); // 3rd party minter mints NFT and memorialize lender positions changePrank(minter); @@ -2372,22 +2066,19 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract tokenId, address(_pool), indexes ); _positionManager.reedemPositions(reedemParams); - _assertLenderLpBalance( - { - lender: lender, - index: 2550, - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: minter, - index: 2550, - lpBalance: 10_000 * 1e27, - depositTime: _startTime - } - ); + + _assertLenderLpBalance({ + lender: lender, + index: 2550, + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: minter, + index: 2550, + lpBalance: 10_000 * 1e18, + depositTime: _startTime + }); } function testMayInteractReverts() external { @@ -2434,13 +2125,11 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract uint256[] memory indexes = new uint256[](1); indexes[0] = 2550; - _addInitialLiquidity( - { - from: testAddress, - amount: 3_000 * 1e18, - index: indexes[0] - } - ); + _addInitialLiquidity({ + from: testAddress, + amount: 3_000 * 1e18, + index: indexes[0] + }); // mint NFT uint256 tokenId = _mintNFT(testAddress, testAddress, address(_pool)); @@ -2454,7 +2143,7 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract assertEq(tokenName(quoteTokenAddress), "Quote"); // allow position manager to take ownership of the position - _pool.approveLpOwnership(address(_positionManager), indexes[0], 3_000 * 1e27); + _pool.approveLpOwnership(address(_positionManager), indexes[0], 3_000 * 1e18); // memorialize position IPositionManagerOwnerActions.MemorializePositionsParams memory memorializeParams = IPositionManagerOwnerActions.MemorializePositionsParams( @@ -2514,85 +2203,67 @@ contract PositionManagerERC721PoolTest is PositionManagerERC721PoolHelperContrac indexes[1] = 2551; indexes[2] = 2552; - _addInitialLiquidity( - { - from: testAddress1, - amount: 3_000 * 1e18, - index: indexes[0] - } - ); - _addInitialLiquidity( - { - from: testAddress1, - amount: 3_000 * 1e18, - index: indexes[1] - } - ); - _addInitialLiquidity( - { - from: testAddress1, - amount: 3_000 * 1e18, - index: indexes[2] - } - ); + _addInitialLiquidity({ + from: testAddress1, + amount: 3_000 * 1e18, + index: indexes[0] + }); + _addInitialLiquidity({ + from: testAddress1, + amount: 3_000 * 1e18, + index: indexes[1] + }); + _addInitialLiquidity({ + from: testAddress1, + amount: 3_000 * 1e18, + index: indexes[2] + }); // mint an NFT to later memorialize existing positions into uint256 tokenId = _mintNFT(testAddress1, testAddress1, address(_pool), keccak256("ERC721_NON_SUBSET_HASH")); // check LPs - _assertLenderLpBalance( - { - lender: testAddress1, - index: indexes[0], - lpBalance: 3_000 * 1e27, - depositTime: currentTime - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: indexes[0], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: testAddress1, - index: indexes[1], - lpBalance: 3_000 * 1e27, - depositTime: currentTime - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: indexes[1], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: testAddress1, - index: indexes[2], - lpBalance: 3_000 * 1e27, - depositTime: currentTime - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: indexes[2], - lpBalance: 0, - depositTime: 0 - } - ); + _assertLenderLpBalance({ + lender: testAddress1, + index: indexes[0], + lpBalance: 3_000 * 1e18, + depositTime: currentTime + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: indexes[0], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: testAddress1, + index: indexes[1], + lpBalance: 3_000 * 1e18, + depositTime: currentTime + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: indexes[1], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: testAddress1, + index: indexes[2], + lpBalance: 3_000 * 1e18, + depositTime: currentTime + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: indexes[2], + lpBalance: 0, + depositTime: 0 + }); // check position manager state - assertEq(_positionManager.getLPTokens(tokenId, indexes[0]), 0); - assertEq(_positionManager.getLPTokens(tokenId, indexes[1]), 0); - assertEq(_positionManager.getLPTokens(tokenId, indexes[2]), 0); + assertEq(_positionManager.getLPs(tokenId, indexes[0]), 0); + assertEq(_positionManager.getLPs(tokenId, indexes[1]), 0); + assertEq(_positionManager.getLPs(tokenId, indexes[2]), 0); assertFalse(_positionManager.isIndexInPosition(tokenId, indexes[0])); assertFalse(_positionManager.isIndexInPosition(tokenId, indexes[1])); assertFalse(_positionManager.isIndexInPosition(tokenId, indexes[2])); @@ -2602,228 +2273,186 @@ contract PositionManagerERC721PoolTest is PositionManagerERC721PoolHelperContrac tokenId, indexes ); // allow position manager to take ownership of the position - _pool.approveLpOwnership(address(_positionManager), indexes[0], 3_000 * 1e27); - _pool.approveLpOwnership(address(_positionManager), indexes[1], 3_000 * 1e27); - _pool.approveLpOwnership(address(_positionManager), indexes[2], 3_000 * 1e27); + _pool.approveLpOwnership(address(_positionManager), indexes[0], 3_000 * 1e18); + _pool.approveLpOwnership(address(_positionManager), indexes[1], 3_000 * 1e18); + _pool.approveLpOwnership(address(_positionManager), indexes[2], 3_000 * 1e18); // memorialize quote tokens into minted NFT vm.expectEmit(true, true, true, true); emit MemorializePosition(testAddress1, tokenId); vm.expectEmit(true, true, true, true); - emit TransferLPTokens(testAddress1, address(_positionManager), indexes, 9_000 * 1e27); + emit TransferLPs(testAddress1, address(_positionManager), indexes, 9_000 * 1e18); _positionManager.memorializePositions(memorializeParams); - _assertLenderLpBalance( - { - lender: testAddress1, - index: indexes[0], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: indexes[0], - lpBalance: 3_000 * 1e27, - depositTime: currentTime - } - ); - _assertLenderLpBalance( - { - lender: testAddress1, - index: indexes[1], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: indexes[1], - lpBalance: 3_000 * 1e27, - depositTime: currentTime - } - ); - _assertLenderLpBalance( - { - lender: testAddress1, - index: indexes[2], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: indexes[2], - lpBalance: 3_000 * 1e27, - depositTime: currentTime - } - ); + _assertLenderLpBalance({ + lender: testAddress1, + index: indexes[0], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: indexes[0], + lpBalance: 3_000 * 1e18, + depositTime: currentTime + }); + _assertLenderLpBalance({ + lender: testAddress1, + index: indexes[1], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: indexes[1], + lpBalance: 3_000 * 1e18, + depositTime: currentTime + }); + _assertLenderLpBalance({ + lender: testAddress1, + index: indexes[2], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: indexes[2], + lpBalance: 3_000 * 1e18, + depositTime: currentTime + }); // check position manager state - assertEq(_positionManager.getLPTokens(tokenId, indexes[0]), 3_000 * 1e27); - assertEq(_positionManager.getLPTokens(tokenId, indexes[1]), 3_000 * 1e27); - assertEq(_positionManager.getLPTokens(tokenId, indexes[2]), 3_000 * 1e27); + assertEq(_positionManager.getLPs(tokenId, indexes[0]), 3_000 * 1e18); + assertEq(_positionManager.getLPs(tokenId, indexes[1]), 3_000 * 1e18); + assertEq(_positionManager.getLPs(tokenId, indexes[2]), 3_000 * 1e18); assertTrue(_positionManager.isIndexInPosition(tokenId, indexes[0])); assertTrue(_positionManager.isIndexInPosition(tokenId, indexes[1])); assertTrue(_positionManager.isIndexInPosition(tokenId, indexes[2])); // add more liquidity - _addInitialLiquidity( - { - from: testAddress1, - amount: 1_000 * 1e18, - index: indexes[0] - } - ); - _addInitialLiquidity( - { - from: testAddress1, - amount: 2_000 * 1e18, - index: indexes[1] - } - ); - _addInitialLiquidity( - { - from: testAddress1, - amount: 3_000 * 1e18, - index: indexes[2] - } - ); + _addInitialLiquidity({ + from: testAddress1, + amount: 1_000 * 1e18, + index: indexes[0] + }); + _addInitialLiquidity({ + from: testAddress1, + amount: 2_000 * 1e18, + index: indexes[1] + }); + _addInitialLiquidity({ + from: testAddress1, + amount: 3_000 * 1e18, + index: indexes[2] + }); // check LP balance - _assertLenderLpBalance( - { - lender: testAddress1, - index: indexes[0], - lpBalance: 1_000 * 1e27, - depositTime: currentTime - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: indexes[0], - lpBalance: 3_000 * 1e27, - depositTime: currentTime - } - ); - _assertLenderLpBalance( - { - lender: testAddress1, - index: indexes[1], - lpBalance: 2_000 * 1e27, - depositTime: currentTime - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: indexes[1], - lpBalance: 3_000 * 1e27, - depositTime: currentTime - } - ); - _assertLenderLpBalance( - { - lender: testAddress1, - index: indexes[2], - lpBalance: 3_000 * 1e27, - depositTime: currentTime - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: indexes[2], - lpBalance: 3_000 * 1e27, - depositTime: currentTime - } - ); + _assertLenderLpBalance({ + lender: testAddress1, + index: indexes[0], + lpBalance: 1_000 * 1e18, + depositTime: currentTime + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: indexes[0], + lpBalance: 3_000 * 1e18, + depositTime: currentTime + }); + _assertLenderLpBalance({ + lender: testAddress1, + index: indexes[1], + lpBalance: 2_000 * 1e18, + depositTime: currentTime + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: indexes[1], + lpBalance: 3_000 * 1e18, + depositTime: currentTime + }); + _assertLenderLpBalance({ + lender: testAddress1, + index: indexes[2], + lpBalance: 3_000 * 1e18, + depositTime: currentTime + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: indexes[2], + lpBalance: 3_000 * 1e18, + depositTime: currentTime + }); // check position manager state - assertEq(_positionManager.getLPTokens(tokenId, indexes[0]), 3_000 * 1e27); - assertEq(_positionManager.getLPTokens(tokenId, indexes[1]), 3_000 * 1e27); - assertEq(_positionManager.getLPTokens(tokenId, indexes[2]), 3_000 * 1e27); + assertEq(_positionManager.getLPs(tokenId, indexes[0]), 3_000 * 1e18); + assertEq(_positionManager.getLPs(tokenId, indexes[1]), 3_000 * 1e18); + assertEq(_positionManager.getLPs(tokenId, indexes[2]), 3_000 * 1e18); assertTrue(_positionManager.isIndexInPosition(tokenId, indexes[0])); assertTrue(_positionManager.isIndexInPosition(tokenId, indexes[1])); assertTrue(_positionManager.isIndexInPosition(tokenId, indexes[2])); // allow position manager to take ownership of the new LPs - _pool.approveLpOwnership(address(_positionManager), indexes[0], 1_000 * 1e27); - _pool.approveLpOwnership(address(_positionManager), indexes[1], 2_000 * 1e27); - _pool.approveLpOwnership(address(_positionManager), indexes[2], 3_000 * 1e27); + _pool.approveLpOwnership(address(_positionManager), indexes[0], 1_000 * 1e18); + _pool.approveLpOwnership(address(_positionManager), indexes[1], 2_000 * 1e18); + _pool.approveLpOwnership(address(_positionManager), indexes[2], 3_000 * 1e18); // rememorialize quote tokens into minted NFT vm.expectEmit(true, true, true, true); emit MemorializePosition(testAddress1, tokenId); vm.expectEmit(true, true, true, true); - emit TransferLPTokens(testAddress1, address(_positionManager), indexes, 6_000 * 1e27); + emit TransferLPs(testAddress1, address(_positionManager), indexes, 6_000 * 1e18); _positionManager.memorializePositions(memorializeParams); // check LP balance - _assertLenderLpBalance( - { - lender: testAddress1, - index: indexes[0], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: indexes[0], - lpBalance: 4_000 * 1e27, - depositTime: currentTime - } - ); - _assertLenderLpBalance( - { - lender: testAddress1, - index: indexes[1], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: indexes[1], - lpBalance: 5_000 * 1e27, - depositTime: currentTime - } - ); - _assertLenderLpBalance( - { - lender: testAddress1, - index: indexes[2], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: indexes[2], - lpBalance: 6_000 * 1e27, - depositTime: currentTime - } - ); + _assertLenderLpBalance({ + lender: testAddress1, + index: indexes[0], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: indexes[0], + lpBalance: 4_000 * 1e18, + depositTime: currentTime + }); + _assertLenderLpBalance({ + lender: testAddress1, + index: indexes[1], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: indexes[1], + lpBalance: 5_000 * 1e18, + depositTime: currentTime + }); + _assertLenderLpBalance({ + lender: testAddress1, + index: indexes[2], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: indexes[2], + lpBalance: 6_000 * 1e18, + depositTime: currentTime + }); // check position manager state - assertEq(_positionManager.getLPTokens(tokenId, indexes[0]), 4_000 * 1e27); - assertEq(_positionManager.getLPTokens(tokenId, indexes[1]), 5_000 * 1e27); - assertEq(_positionManager.getLPTokens(tokenId, indexes[2]), 6_000 * 1e27); + assertEq(_positionManager.getLPs(tokenId, indexes[0]), 4_000 * 1e18); + assertEq(_positionManager.getLPs(tokenId, indexes[1]), 5_000 * 1e18); + assertEq(_positionManager.getLPs(tokenId, indexes[2]), 6_000 * 1e18); assertTrue(_positionManager.isIndexInPosition(tokenId, indexes[0])); assertTrue(_positionManager.isIndexInPosition(tokenId, indexes[1])); assertTrue(_positionManager.isIndexInPosition(tokenId, indexes[2])); // construct move liquidity params IPositionManagerOwnerActions.MoveLiquidityParams memory moveLiquidityParams = IPositionManagerOwnerActions.MoveLiquidityParams( - tokenId, address(_pool), indexes[0], indexes[1] + tokenId, address(_pool), indexes[0], indexes[1], block.timestamp + 30 ); // move liquidity called by testAddress1 @@ -2833,59 +2462,47 @@ contract PositionManagerERC721PoolTest is PositionManagerERC721PoolHelperContrac _positionManager.moveLiquidity(moveLiquidityParams); // check LP balance - _assertLenderLpBalance( - { - lender: testAddress1, - index: indexes[0], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: indexes[0], - lpBalance: 0, - depositTime: currentTime - } - ); - _assertLenderLpBalance( - { - lender: testAddress1, - index: indexes[1], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: indexes[1], - lpBalance: 9_000 * 1e27, - depositTime: currentTime - } - ); - _assertLenderLpBalance( - { - lender: testAddress1, - index: indexes[2], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: indexes[2], - lpBalance: 6_000 * 1e27, - depositTime: currentTime - } - ); + _assertLenderLpBalance({ + lender: testAddress1, + index: indexes[0], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: indexes[0], + lpBalance: 0, + depositTime: currentTime + }); + _assertLenderLpBalance({ + lender: testAddress1, + index: indexes[1], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: indexes[1], + lpBalance: 9_000 * 1e18, + depositTime: currentTime + }); + _assertLenderLpBalance({ + lender: testAddress1, + index: indexes[2], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: indexes[2], + lpBalance: 6_000 * 1e18, + depositTime: currentTime + }); // check position manager state - assertEq(_positionManager.getLPTokens(tokenId, indexes[0]), 0); - assertEq(_positionManager.getLPTokens(tokenId, indexes[1]), 9_000 * 1e27); - assertEq(_positionManager.getLPTokens(tokenId, indexes[2]), 6_000 * 1e27); + assertEq(_positionManager.getLPs(tokenId, indexes[0]), 0); + assertEq(_positionManager.getLPs(tokenId, indexes[1]), 9_000 * 1e18); + assertEq(_positionManager.getLPs(tokenId, indexes[2]), 6_000 * 1e18); assertFalse(_positionManager.isIndexInPosition(tokenId, indexes[0])); assertTrue(_positionManager.isIndexInPosition(tokenId, indexes[1])); assertTrue(_positionManager.isIndexInPosition(tokenId, indexes[2])); @@ -2928,83 +2545,65 @@ contract PositionManagerERC721PoolTest is PositionManagerERC721PoolHelperContrac _positionManager.reedemPositions(reedemParams); // check pool state - _assertLenderLpBalance( - { - lender: testAddress1, - index: indexes[0], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: testAddress2, - index: indexes[0], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: indexes[0], - lpBalance: 0, - depositTime: currentTime - } - ); - _assertLenderLpBalance( - { - lender: testAddress1, - index: indexes[0], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: testAddress2, - index: indexes[1], - lpBalance: 9_000 * 1e27, - depositTime: currentTime - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: indexes[1], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: testAddress1, - index: indexes[0], - lpBalance: 0, - depositTime: 0 - } - ); - _assertLenderLpBalance( - { - lender: testAddress2, - index: indexes[2], - lpBalance: 6_000 * 1e27, - depositTime: currentTime - } - ); - _assertLenderLpBalance( - { - lender: address(_positionManager), - index: indexes[2], - lpBalance: 0, - depositTime: 0 - } - ); + _assertLenderLpBalance({ + lender: testAddress1, + index: indexes[0], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: testAddress2, + index: indexes[0], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: indexes[0], + lpBalance: 0, + depositTime: currentTime + }); + _assertLenderLpBalance({ + lender: testAddress1, + index: indexes[0], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: testAddress2, + index: indexes[1], + lpBalance: 9_000 * 1e18, + depositTime: currentTime + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: indexes[1], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: testAddress1, + index: indexes[0], + lpBalance: 0, + depositTime: 0 + }); + _assertLenderLpBalance({ + lender: testAddress2, + index: indexes[2], + lpBalance: 6_000 * 1e18, + depositTime: currentTime + }); + _assertLenderLpBalance({ + lender: address(_positionManager), + index: indexes[2], + lpBalance: 0, + depositTime: 0 + }); // check position manager state - assertEq(_positionManager.getLPTokens(tokenId, indexes[0]), 0); - assertEq(_positionManager.getLPTokens(tokenId, indexes[1]), 0); - assertEq(_positionManager.getLPTokens(tokenId, indexes[2]), 0); + assertEq(_positionManager.getLPs(tokenId, indexes[0]), 0); + assertEq(_positionManager.getLPs(tokenId, indexes[1]), 0); + assertEq(_positionManager.getLPs(tokenId, indexes[2]), 0); assertFalse(_positionManager.isIndexInPosition(tokenId, indexes[0])); assertFalse(_positionManager.isIndexInPosition(tokenId, indexes[1])); assertFalse(_positionManager.isIndexInPosition(tokenId, indexes[2])); diff --git a/tests/forge/RewardsManager.t.sol b/tests/forge/RewardsManager.t.sol index cbfe9578a..2bad9d0b7 100644 --- a/tests/forge/RewardsManager.t.sol +++ b/tests/forge/RewardsManager.t.sol @@ -160,8 +160,15 @@ contract RewardsManagerTest is DSTestPlus { _rewardsManager.unstake(tokenId); assertEq(_positionManager.ownerOf(tokenId), minter); - // check token was transferred to rewards contract + // check token was transferred from rewards contract to minter assertEq(_positionManager.ownerOf(tokenId), address(minter)); + + // invariant: all bucket snapshots are removed for the token id that was unstaken + for(uint256 bucketIndex = 0; bucketIndex <= 7388; bucketIndex++) { + (uint256 lps, uint256 rate) = _rewardsManager.getBucketStateStakeInfo(tokenId, bucketIndex); + assertEq(lps, 0); + assertEq(rate, 0); + } } function _triggerReserveAuctionsNoTake(TriggerReserveAuctionParams memory params_) internal { @@ -179,7 +186,7 @@ contract RewardsManagerTest is DSTestPlus { // borrower repays some of their debt, providing reserves to be claimed // don't pull any collateral, as such functionality is unrelated to reserve auctions - params_.pool.repayDebt(borrower, Maths.wdiv(params_.borrowAmount, Maths.wad(2)), 0); + params_.pool.repayDebt(borrower, Maths.wdiv(params_.borrowAmount, Maths.wad(2)), 0, borrower, MAX_FENWICK_INDEX); // start reserve auction changePrank(_bidder); @@ -240,7 +247,7 @@ contract RewardsManagerTest is DSTestPlus { tokenId_ = _positionManager.mint(mintParams); for (uint256 i = 0; i < params_.indexes.length; i++) { - params_.pool.addQuoteToken(params_.mintAmount, params_.indexes[i]); + params_.pool.addQuoteToken(params_.mintAmount, params_.indexes[i], type(uint256).max); (uint256 lpBalance, ) = params_.pool.lenderInfo(params_.indexes[i], params_.minter); params_.pool.approveLpOwnership(address(_positionManager), params_.indexes[i], lpBalance); } @@ -279,7 +286,7 @@ contract RewardsManagerTest is DSTestPlus { // borrower repays some of their debt, providing reserves to be claimed // don't pull any collateral, as such functionality is unrelated to reserve auctions - params_.pool.repayDebt(borrower, params_.borrowAmount, 0); + params_.pool.repayDebt(borrower, params_.borrowAmount, 0, borrower, MAX_FENWICK_INDEX); // start reserve auction changePrank(_bidder); @@ -408,7 +415,7 @@ contract RewardsManagerTest is DSTestPlus { // check rewards earned uint256 rewardsEarned = _rewardsManager.calculateRewards(tokenIdOne, currentBurnEpoch); - assertEq(rewardsEarned, 40.214136545950568150 * 1e18); + assertEq(rewardsEarned, 40.214136545950568100 * 1e18); // claim rewards accrued since deposit changePrank(_minterOne); @@ -504,7 +511,7 @@ contract RewardsManagerTest is DSTestPlus { pool: address(_poolOne), tokenId: tokenIdOne, claimedArray: _epochsClaimedArray(2, 0), - reward: 80.793427892333608620 * 1e18, + reward: 80.793427892333608615 * 1e18, updateRatesReward: 3.689026486034825940 * 1e18 }); } @@ -623,7 +630,7 @@ contract RewardsManagerTest is DSTestPlus { // borrower1 repays their loan (uint256 debt, , ) = _poolOne.borrowerInfo(borrower1); - _poolOne.repayDebt(borrower1, debt, 0); + _poolOne.repayDebt(borrower1, debt, 0, borrower1, MAX_FENWICK_INDEX); /*****************************/ /*** First Reserve Auction ***/ @@ -683,7 +690,7 @@ contract RewardsManagerTest is DSTestPlus { // borrower1 repays their loan again changePrank(borrower1); (debt, , ) = _poolOne.borrowerInfo(borrower1); - _poolOne.repayDebt(borrower1, debt, 0); + _poolOne.repayDebt(borrower1, debt, 0, borrower1, MAX_FENWICK_INDEX); // recorder updates the change in exchange rates in the second index _updateExchangeRates({ @@ -704,7 +711,7 @@ contract RewardsManagerTest is DSTestPlus { pool: address(_poolOne), tokenId: tokenIdOne, claimedArray: _epochsClaimedArray(1, 0), - reward: 0.298393183929769729 * 1e18, + reward: 0.298393183929769450 * 1e18, updateRatesReward: 0 }); } @@ -759,7 +766,7 @@ contract RewardsManagerTest is DSTestPlus { assertEq(_ajnaToken.balanceOf(_updater), 18.914328218434904846 * 1e18); uint256 rewardsEarned = _rewardsManager.calculateRewards(tokenIdOne, _poolOne.currentBurnEpoch()); - assertEq(rewardsEarned, 189.143282184349085719 * 1e18); + assertEq(rewardsEarned, 189.143282184349085696 * 1e18); assertLt(rewardsEarned, Maths.wmul(totalTokensBurned, 0.800000000000000000 * 1e18)); /******************************/ @@ -784,7 +791,7 @@ contract RewardsManagerTest is DSTestPlus { // check available rewards rewardsEarned = _rewardsManager.calculateRewards(tokenIdOne, _poolOne.currentBurnEpoch()); - assertEq(rewardsEarned, 354.209322508542220912 * 1e18); + assertEq(rewardsEarned, 354.209322508542220207 * 1e18); assertLt(rewardsEarned, Maths.wmul(totalTokensBurned, 0.800000000000000000 * 1e18)); /*****************************/ @@ -801,7 +808,7 @@ contract RewardsManagerTest is DSTestPlus { // skip updating exchange rates and check available rewards uint256 rewardsEarnedNoUpdate = _rewardsManager.calculateRewards(tokenIdOne, _poolOne.currentBurnEpoch()); - assertEq(rewardsEarnedNoUpdate, 354.209322508542220912 * 1e18); + assertEq(rewardsEarnedNoUpdate, 354.209322508542220207 * 1e18); assertLt(rewardsEarned, Maths.wmul(totalTokensBurned, 0.800000000000000000 * 1e18)); // snapshot calling update exchange rate @@ -817,7 +824,7 @@ contract RewardsManagerTest is DSTestPlus { // check available rewards rewardsEarned = _rewardsManager.calculateRewards(tokenIdOne, _poolOne.currentBurnEpoch()); - assertEq(rewardsEarned, 489.772410159936903182 * 1e18); + assertEq(rewardsEarned, 489.772410159936902605 * 1e18); assertGt(rewardsEarned, rewardsEarnedNoUpdate); assertLt(rewardsEarned, Maths.wmul(totalTokensBurned, 0.800000000000000000 * 1e18)); @@ -838,7 +845,7 @@ contract RewardsManagerTest is DSTestPlus { // check rewards earned rewardsEarned = _rewardsManager.calculateRewards(tokenIdOne, _poolOne.currentBurnEpoch()); - assertEq(rewardsEarned, 354.209322508542220912 * 1e18); + assertEq(rewardsEarned, 354.209322508542220207 * 1e18); // call update exchange rate changePrank(_updater2); @@ -850,7 +857,7 @@ contract RewardsManagerTest is DSTestPlus { // check rewards earned won't increase since previous update was missed rewardsEarned = _rewardsManager.calculateRewards(tokenIdOne, _poolOne.currentBurnEpoch()); - assertEq(rewardsEarned, 354.209322508542220912 * 1e18); + assertEq(rewardsEarned, 354.209322508542220207 * 1e18); /*****************************/ /*** Fifth Reserve Auction ***/ @@ -873,7 +880,7 @@ contract RewardsManagerTest is DSTestPlus { assertEq(_ajnaToken.balanceOf(_updater2), 10.978967849507124864 * 1e18); rewardsEarned = _rewardsManager.calculateRewards(tokenIdOne, _poolOne.currentBurnEpoch()); - assertEq(rewardsEarned, 463.999001003613678404 * 1e18); + assertEq(rewardsEarned, 463.999001003613677587 * 1e18); // claim all rewards accrued since deposit changePrank(_minterOne); @@ -904,11 +911,11 @@ contract RewardsManagerTest is DSTestPlus { }); uint256 tokenIdTwo = _mintAndMemorializePositionNFT(mintMemorializeParams); // bucket exchange rates are not changed at the time minter two stakes - assertEq(_poolOne.bucketExchangeRate(2550), 1e27); - assertEq(_poolOne.bucketExchangeRate(2551), 1e27); - assertEq(_poolOne.bucketExchangeRate(2552), 1e27); - assertEq(_poolOne.bucketExchangeRate(2553), 1e27); - assertEq(_poolOne.bucketExchangeRate(2555), 1e27); + assertEq(_poolOne.bucketExchangeRate(2550), 1e18); + assertEq(_poolOne.bucketExchangeRate(2551), 1e18); + assertEq(_poolOne.bucketExchangeRate(2552), 1e18); + assertEq(_poolOne.bucketExchangeRate(2553), 1e18); + assertEq(_poolOne.bucketExchangeRate(2555), 1e18); _stakeToken(address(_poolOne), _minterTwo, tokenIdTwo); // borrower borrows and change the exchange rates of buckets @@ -928,11 +935,11 @@ contract RewardsManagerTest is DSTestPlus { }); uint256 tokenIdThree = _mintAndMemorializePositionNFT(mintMemorializeParams); // bucket exchange rates are higher at the time minter three stakes - assertEq(_poolOne.bucketExchangeRate(2550), 1.000000116565164638999999999 * 1e27); - assertEq(_poolOne.bucketExchangeRate(2551), 1.000000116565164638999999999 * 1e27); - assertEq(_poolOne.bucketExchangeRate(2552), 1.000000116565164638999999999 * 1e27); - assertEq(_poolOne.bucketExchangeRate(2553), 1.000000116565164638999999999 * 1e27); - assertEq(_poolOne.bucketExchangeRate(2555), 1.000000116565164638999999999 * 1e27); + assertEq(_poolOne.bucketExchangeRate(2550), 1.000000116565164639 * 1e18); + assertEq(_poolOne.bucketExchangeRate(2551), 1.000000116565164639 * 1e18); + assertEq(_poolOne.bucketExchangeRate(2552), 1.000000116565164639 * 1e18); + assertEq(_poolOne.bucketExchangeRate(2553), 1.000000116565164639 * 1e18); + assertEq(_poolOne.bucketExchangeRate(2555), 1.000000116565164639 * 1e18); _stakeToken(address(_poolOne), _minterThree, tokenIdThree); skip(1 days); @@ -951,30 +958,26 @@ contract RewardsManagerTest is DSTestPlus { pool: address(_poolOne), tokenId: tokenIdTwo, claimedArray: _epochsClaimedArray(1, 0), - reward: 20.035397317001861785 * 1e18, + reward: 20.035397317001861795 * 1e18, updateRatesReward: 0 }); uint256 minterTwoBalance = _ajnaToken.balanceOf(_minterTwo); - assertEq(minterTwoBalance, 20.035397317001861785 * 1e18); + assertEq(minterTwoBalance, 20.035397317001861795 * 1e18); _unstakeToken({ minter: _minterThree, pool: address(_poolOne), tokenId: tokenIdThree, claimedArray: _epochsClaimedArray(1, 0), - reward: 16.692493739675876000 * 1e18, + reward: 16.692493739675875940 * 1e18, updateRatesReward: 0 }); uint256 minterThreeBalance = _ajnaToken.balanceOf(_minterThree); - assertEq(minterThreeBalance, 16.692493739675876000 * 1e18); + assertEq(minterThreeBalance, 16.692493739675875940 * 1e18); assertGt(minterTwoBalance, minterThreeBalance); } - function testMultiPeriodRewardsMultiClaim() external { - - } - // Calling updateExchangeRates not needed since deposits will update the exchange rate themselves function testClaimRewardsMultipleDepositsSameBucketsMultipleAuctions() external { skip(10); @@ -1180,7 +1183,7 @@ contract RewardsManagerTest is DSTestPlus { updater: _updater, pool: address(_poolOne), depositIndexes: depositIndexes, - reward: 11.241216009399483348 * 1e18 + reward: 11.241216009399483350 * 1e18 }); _triggerReserveAuctions(TriggerReserveAuctionParams({ @@ -1209,24 +1212,24 @@ contract RewardsManagerTest is DSTestPlus { pool: address(_poolOne), epoch: 1, timestamp: block.timestamp - (52 weeks + 72 hours), - interest: 6447445050021308895, - burned: 81574747191341355205 + interest: 6.447445050021308895 * 1e18, + burned: 81.574747191341355205 * 1e18 }); _assertBurn({ pool: address(_poolOne), epoch: 2, timestamp: block.timestamp - (26 weeks + 48 hours), - burned: 306399067379332449973, - interest: 23974564976746846096 + burned: 306.399067379332450033 * 1e18, + interest: 23.974564976746846096 * 1e18 }); _assertBurn({ pool: address(_poolOne), epoch: 3, timestamp: block.timestamp - 24 hours, - burned: 699814215483322160364, - interest: 55764974712671474765 + burned: 699.814215483322160424 * 1e18, + interest: 55.764974712671474765 * 1e18 }); // both stakers claim rewards @@ -1235,7 +1238,7 @@ contract RewardsManagerTest is DSTestPlus { pool: address(_poolOne), tokenId: tokenIdOne, claimedArray: _epochsClaimedArray(3, 0), - reward: 58.317851290276861885 * 1e18, + reward: 58.317851290276861945 * 1e18, updateRatesReward: 0 }); @@ -1244,7 +1247,7 @@ contract RewardsManagerTest is DSTestPlus { pool: address(_poolOne), tokenId: tokenIdTwo, claimedArray: _epochsClaimedArray(3, 0), - reward: 291.589256451384309685 * 1e18, + reward: 291.589256451384309690 * 1e18, updateRatesReward: 0 }); } @@ -1328,14 +1331,41 @@ contract RewardsManagerTest is DSTestPlus { assertGt(_ajnaToken.balanceOf(_updater), 0); // check owner can withdraw the NFT and rewards will be automatically claimed + + uint256 snapshot = vm.snapshot(); + + // claimed rewards amount is greater than available tokens in rewards manager contract + + // burn rewards manager tokens and leave only 5 tokens available + changePrank(address(_rewardsManager)); + IERC20Token(address(_ajnaToken)).burn(99_999_990.978586345404952410 * 1e18); + + uint256 managerBalance = _ajnaToken.balanceOf(address(_rewardsManager)); + assertEq(managerBalance, 5 * 1e18); + changePrank(_minterOne); vm.expectEmit(true, true, true, true); - emit ClaimRewards(_minterOne, address(_poolOne), tokenIdOne, _epochsClaimedArray(1, 0), 40.214136545950568150 * 1e18); + emit ClaimRewards(_minterOne, address(_poolOne), tokenIdOne, _epochsClaimedArray(1, 0), 40.214136545950568100 * 1e18); + vm.expectEmit(true, true, true, true); + emit Unstake(_minterOne, address(_poolOne), tokenIdOne); + _rewardsManager.unstake(tokenIdOne); + + // minter one receives only the amount of 5 ajna tokens available in manager balance instead calculated rewards of 40.214136545950568150 + assertEq(_ajnaToken.balanceOf(_minterOne), managerBalance); + // all 5 tokens available in manager balance were used to reward minter one + assertEq(_ajnaToken.balanceOf(address(_rewardsManager)), 0); + + vm.revertTo(snapshot); + + // test when enough tokens in rewards manager contracts + changePrank(_minterOne); + vm.expectEmit(true, true, true, true); + emit ClaimRewards(_minterOne, address(_poolOne), tokenIdOne, _epochsClaimedArray(1, 0), 40.214136545950568100 * 1e18); vm.expectEmit(true, true, true, true); emit Unstake(_minterOne, address(_poolOne), tokenIdOne); _rewardsManager.unstake(tokenIdOne); assertEq(_positionManager.ownerOf(tokenIdOne), _minterOne); - assertEq(_ajnaToken.balanceOf(_minterOne), 40.214136545950568150 * 1e18); + assertEq(_ajnaToken.balanceOf(_minterOne), 40.214136545950568100 * 1e18); assertLt(_ajnaToken.balanceOf(_minterOne), tokensToBurn); uint256 currentBurnEpoch = _poolOne.currentBurnEpoch(); @@ -1376,6 +1406,7 @@ contract RewardsManagerTest is DSTestPlus { mintAmount: 1000 * 1e18, pool: _poolTwo }); + uint256 tokenIdTwo = _mintAndMemorializePositionNFT(mintMemorializeParams); // minterOne deposits their NFT into the rewards contract @@ -1391,6 +1422,7 @@ contract RewardsManagerTest is DSTestPlus { limitIndex: 3, pool: _poolOne }); + uint256 tokensToBurn = _triggerReserveAuctions(triggerReserveAuctionParams); uint256 currentBurnEpochPoolOne = _poolOne.currentBurnEpoch(); @@ -1418,9 +1450,9 @@ contract RewardsManagerTest is DSTestPlus { changePrank(_minterOne); assertEq(_ajnaToken.balanceOf(_minterOne), 4.021413654595047590 * 1e18); vm.expectEmit(true, true, true, true); - emit ClaimRewards(_minterOne, address(_poolOne), tokenIdOne, _epochsClaimedArray(1, 0), 40.214136545950568150 * 1e18); + emit ClaimRewards(_minterOne, address(_poolOne), tokenIdOne, _epochsClaimedArray(1, 0), 40.214136545950568100 * 1e18); _rewardsManager.claimRewards(tokenIdOne, currentBurnEpochPoolOne); - assertEq(_ajnaToken.balanceOf(_minterOne), 44.235550200545615740 * 1e18); + assertEq(_ajnaToken.balanceOf(_minterOne), 44.235550200545615690 * 1e18); assertLt(_ajnaToken.balanceOf(_minterOne), tokensToBurn); } @@ -1596,4 +1628,34 @@ contract RewardsManagerTest is DSTestPlus { } } + function testClaimRewardsFreezeUnclaimedYield() external { + skip(10); + + uint256[] memory depositIndexes = new uint256[](5); + depositIndexes[0] = 9; + depositIndexes[1] = 1; + depositIndexes[2] = 2; + depositIndexes[3] = 3; + depositIndexes[4] = 4; + MintAndMemorializeParams memory mintMemorializeParams = MintAndMemorializeParams({ + indexes: depositIndexes, + minter: _minterOne, + mintAmount: 1000 * 1e18, + pool: _poolOne + }); + + uint256 tokenIdOne = _mintAndMemorializePositionNFT(mintMemorializeParams); + _stakeToken(address(_poolOne), _minterOne, tokenIdOne); + + uint256 currentBurnEpoch = _poolOne.currentBurnEpoch(); + + changePrank(_minterOne); + // should revert if the epoch to claim is not available yet + vm.expectRevert(IRewardsManagerErrors.EpochNotAvailable.selector); + _rewardsManager.claimRewards(tokenIdOne, currentBurnEpoch + 10); + + // user should be able to claim rewards for current epoch + _rewardsManager.claimRewards(tokenIdOne, currentBurnEpoch); + } + } diff --git a/tests/forge/interactions/BalancerUniswapExample.sol b/tests/forge/interactions/BalancerUniswapExample.sol index 5b83d185c..d3b7c6eec 100644 --- a/tests/forge/interactions/BalancerUniswapExample.sol +++ b/tests/forge/interactions/BalancerUniswapExample.sol @@ -134,9 +134,9 @@ contract BalancerUniswapPurchaser { // approve ajna pool to transfer flash loaned collateral collateral.approve(decoded.ajnaPool, loanAmount); // purchase USDC with 1 WETH from ajna - uint256 lps = IAjnaPool(decoded.ajnaPool).addCollateral(loanAmount, decoded.bucketIndex); + uint256 lps = IAjnaPool(decoded.ajnaPool).addCollateral(loanAmount, decoded.bucketIndex, block.timestamp + 5 minutes); (uint256 quoteAmount, ) = IAjnaPool(decoded.ajnaPool).removeQuoteToken(type(uint256).max, decoded.bucketIndex); - assert(lps == 83008350.10362729922336157 * 1e27); // LPS in bucket + assert(lps == 83008350.10362729922336157 * 1e18); // LPS in bucket assert(quoteAmount == 4995.19230769230769 * 1e18); // Purchased quote amount assert(quote.balanceOf(address(this)) == 4995.192307 * 1e6); // USDC balance after Ajna purchase assert(collateral.balanceOf(address(this)) == 0); // WETH balance after Ajna purchase diff --git a/tests/forge/interactions/ERC20TakeWithExternalLiquidity.t.sol b/tests/forge/interactions/ERC20TakeWithExternalLiquidity.t.sol index f16a77e10..330352a96 100644 --- a/tests/forge/interactions/ERC20TakeWithExternalLiquidity.t.sol +++ b/tests/forge/interactions/ERC20TakeWithExternalLiquidity.t.sol @@ -54,11 +54,11 @@ contract ERC20TakeWithExternalLiquidityTest is Test { // add liquidity to the Ajna pool vm.startPrank(_lender); usdc.approve(address(_ajnaPool), type(uint256).max); - _ajnaPool.addQuoteToken(2_000 * 1e18, 3696); - _ajnaPool.addQuoteToken(5_000 * 1e18, 3698); - _ajnaPool.addQuoteToken(11_000 * 1e18, 3700); - _ajnaPool.addQuoteToken(25_000 * 1e18, 3702); - _ajnaPool.addQuoteToken(30_000 * 1e18, 3704); + _ajnaPool.addQuoteToken(2_000 * 1e18, 3696, type(uint256).max); + _ajnaPool.addQuoteToken(5_000 * 1e18, 3698, type(uint256).max); + _ajnaPool.addQuoteToken(11_000 * 1e18, 3700, type(uint256).max); + _ajnaPool.addQuoteToken(25_000 * 1e18, 3702, type(uint256).max); + _ajnaPool.addQuoteToken(30_000 * 1e18, 3704, type(uint256).max); vm.stopPrank(); // borrower draws debt @@ -132,4 +132,27 @@ contract ERC20TakeWithExternalLiquidityTest is Test { // confirm we earned some quote token assertGt(usdc.balanceOf(address(taker)), 0); } + + function testTakeCalleeDiffersFromSender() external { + + // _lender is msg.sender, QT & CT balances pre take + assertEq(usdc.balanceOf(_lender), 119_999.999999926999804658 * 1e18); + assertEq(weth.balanceOf(_lender), 0); + + // callee, _lender1 QT & CT balances pre take + assertEq(usdc.balanceOf(_lender1), 120_000.0 * 1e18); + assertEq(weth.balanceOf(_lender1), 4.0 * 1e18); + + // lender calls take, passing _lender1 as the callee + changePrank(_lender); + _ajnaPool.take(_borrower, 1_001 * 1e18, _lender1, new bytes(0)); + + // _lender is has QT deducted from balance + assertEq(usdc.balanceOf(_lender), 119_999.999999926985301196 * 1e18); + assertEq(weth.balanceOf(_lender), 0); + + // callee, _lender1 receives CT from take + assertEq(usdc.balanceOf(_lender1), 120_000.0 * 1e18); + assertEq(weth.balanceOf(_lender1), 6.0 * 1e18); + } } diff --git a/tests/forge/interactions/ERC721TakeWithExternalLiquidity.sol b/tests/forge/interactions/ERC721TakeWithExternalLiquidity.sol index 8e3327282..927b22a58 100644 --- a/tests/forge/interactions/ERC721TakeWithExternalLiquidity.sol +++ b/tests/forge/interactions/ERC721TakeWithExternalLiquidity.sol @@ -36,13 +36,11 @@ contract ERC721TakeWithExternalLiquidityTest is ERC721HelperContract { _quote.approve(address(_pool), type(uint256).max); // lender deposits 50_000 Quote into the pool - _addInitialLiquidity( - { - from: _lender, - amount: 50_000 * 1e18, - index: _i1004_98 - } - ); + _addInitialLiquidity({ + from: _lender, + amount: 50_000 * 1e18, + index: _i1004_98 + }); uint256[] memory tokenIdsToAdd = new uint256[](2); tokenIdsToAdd[0] = 1; @@ -90,4 +88,35 @@ contract ERC721TakeWithExternalLiquidityTest is ERC721HelperContract { // confirm we earned some quote token assertEq(_quote.balanceOf(address(taker)), 970.423096682230524352 * 1e18); } + + function testTakeNFTCalleeDiffersFromSender() external { + // instantiate and fund a hypothetical NFT marketplace + NFTMarketPlace marketPlace = new NFTMarketPlace(_quote); + deal(address(_quote), address(marketPlace), 25_000 * 1e18); + + // instantiate a taker contract which implements IERC721Taker and uses this marketplace + NFTTakeExample taker = new NFTTakeExample(address(marketPlace)); + changePrank(address(taker)); + assertEq(_quote.balanceOf(address(taker)), 0); + _quote.approve(address(_pool), type(uint256).max); + _collateral.setApprovalForAll(address(marketPlace), true); + + // _lender is msg.sender, QT & CT balances pre take + assertEq(_quote.balanceOf(_lender), 49_979.825641778370686641 * 1e18); + assertEq(_quote.balanceOf(address(taker)), 0); + + // call take using taker contract + changePrank(_lender); + bytes memory data = abi.encode(address(_pool)); + vm.expectEmit(true, true, false, true); + uint256 quoteTokenPaid = 529.576903317769475648 * 1e18; + uint256 collateralPurchased = 2 * 1e18; + uint256 bondChange = 5.295769033177694756 * 1e18; + emit Take(_borrower, quoteTokenPaid, collateralPurchased, bondChange, true); + _pool.take(_borrower, 2, address(taker), data); + + // _lender is msg.sender, QT & CT balances post take + assertEq(_quote.balanceOf(_lender), 49_450.248738460601210993 * 1e18); + assertEq(_quote.balanceOf(address(taker)), 1_500.0 * 1e18); // QT is increased as NFTTakeExample contract sells the NFT + } } diff --git a/tests/forge/interactions/Interfaces.sol b/tests/forge/interactions/Interfaces.sol index e00c7b003..3ed9cd8a6 100644 --- a/tests/forge/interactions/Interfaces.sol +++ b/tests/forge/interactions/Interfaces.sol @@ -13,7 +13,8 @@ interface IAjnaPool { function addCollateral( uint256 amount, - uint256 index + uint256 index, + uint256 expiry ) external returns (uint256 lpbChange); function removeQuoteToken( diff --git a/tests/forge/interactions/NFTTakeExample.sol b/tests/forge/interactions/NFTTakeExample.sol index 9a1e91687..78d730e26 100644 --- a/tests/forge/interactions/NFTTakeExample.sol +++ b/tests/forge/interactions/NFTTakeExample.sol @@ -58,6 +58,30 @@ contract NFTMarketPlace is INFTMarketPlace { currency.transfer(msg.sender, collectionOffer); } + /** @notice Implementing this method allows contracts to receive ERC721 tokens + * @dev https://forum.openzeppelin.com/t/erc721holder-ierc721receiver-and-onerc721received/11828 + */ + function onERC721Received(address, address, uint256, bytes memory) external pure returns (bytes4) { + return this.onERC721Received.selector; + } +} + +contract NFTNoopTakeExample is IERC721Taker { + uint256[] public tokenIdsReceived; + uint256 public quoteAmountDueReceived; + address public poolAddressReceived; + + function atomicSwapCallback( + uint256[] memory tokenIds, + uint256 quoteAmountDue, + bytes calldata data + ) external { + // NOOP, records inputs passed from pool to be checked in tests + tokenIdsReceived = tokenIds; + quoteAmountDueReceived = quoteAmountDue; + poolAddressReceived = abi.decode(data, (address)); + } + /** @notice Implementing this method allows contracts to receive ERC721 tokens * @dev https://forum.openzeppelin.com/t/erc721holder-ierc721receiver-and-onerc721received/11828 */ diff --git a/tests/forge/interactions/PurchaseQuoteWithExternalLiquidity.t.sol b/tests/forge/interactions/PurchaseQuoteWithExternalLiquidity.t.sol index 481be68c8..e9dfd6cfe 100644 --- a/tests/forge/interactions/PurchaseQuoteWithExternalLiquidity.t.sol +++ b/tests/forge/interactions/PurchaseQuoteWithExternalLiquidity.t.sol @@ -28,7 +28,7 @@ contract PurchaseQuoteWithExternalLiquidityTest is Test { deal(USDC, _lender, 120_000 * 1e6); vm.startPrank(_lender); usdc.approve(address(_ajnaPool), type(uint256).max); - _ajnaPool.addQuoteToken(5_000 * 1e18, 500); + _ajnaPool.addQuoteToken(5_000 * 1e18, 500, type(uint256).max); vm.stopPrank(); } diff --git a/tests/forge/utils/DSTestPlus.sol b/tests/forge/utils/DSTestPlus.sol index dcc00ab2e..ef1f25d0c 100644 --- a/tests/forge/utils/DSTestPlus.sol +++ b/tests/forge/utils/DSTestPlus.sol @@ -22,7 +22,7 @@ abstract contract DSTestPlus is Test, IPoolEvents { using EnumerableSet for EnumerableSet.UintSet; // nonce for generating random addresses - uint16 internal _nonce = 0; + uint256 internal _nonce = 0; // mainnet address of AJNA token, because tests are forked address internal _ajna = 0x9a96ec9B57Fb64FbC60B423d1f4da7691Bd35079; @@ -104,7 +104,7 @@ abstract contract DSTestPlus is Test, IPoolEvents { uint256 index ) internal { uint256 quoteTokenScale = IPool(address(_pool)).quoteTokenScale(); - uint256 lpAmount = (amount / quoteTokenScale) * quoteTokenScale * 1e9; + uint256 lpAmount = (amount / quoteTokenScale) * quoteTokenScale; _addLiquidity(from, amount, index, lpAmount, MAX_PRICE); } @@ -115,7 +115,7 @@ abstract contract DSTestPlus is Test, IPoolEvents { uint256 index ) internal { changePrank(from); - _pool.addQuoteToken(amount, index); + _pool.addQuoteToken(amount, index, type(uint256).max); // Add for tearDown lenders.add(from); @@ -136,7 +136,7 @@ abstract contract DSTestPlus is Test, IPoolEvents { vm.expectEmit(true, true, false, true); emit AddQuoteToken(from, index, (amount / quoteTokenScale) * quoteTokenScale, lpAward, newLup); _assertQuoteTokenTransferEvent(from, address(_pool), amount); - _pool.addQuoteToken(amount, index); + _pool.addQuoteToken(amount, index, type(uint256).max); // Add for tearDown lenders.add(from); @@ -246,7 +246,7 @@ abstract contract DSTestPlus is Test, IPoolEvents { vm.expectEmit(true, true, false, true); emit Kick(borrower, debt, collateral, bond); vm.expectEmit(true, true, false, true); - emit RemoveQuoteToken(from, index, removedFromDeposit, removedFromDeposit * 1e9, lup); + emit RemoveQuoteToken(from, index, removedFromDeposit, removedFromDeposit, lup); if(transferAmount != 0) _assertQuoteTokenTransferEvent(from, address(_pool), transferAmount); _pool.kickWithDeposit(index); } @@ -276,7 +276,7 @@ abstract contract DSTestPlus is Test, IPoolEvents { changePrank(from); vm.expectEmit(true, true, true, true); emit MoveQuoteToken(from, fromIndex, toIndex, amountMoved, lpRedeemFrom, lpAwardTo, newLup); - (uint256 lpbFrom, uint256 lpbTo) = _pool.moveQuoteToken(amount, fromIndex, toIndex); + (uint256 lpbFrom, uint256 lpbTo) = _pool.moveQuoteToken(amount, fromIndex, toIndex, type(uint256).max); assertEq(lpbFrom, lpRedeemFrom); assertEq(lpbTo, lpAwardTo); @@ -711,7 +711,7 @@ abstract contract DSTestPlus is Test, IPoolEvents { ) internal { changePrank(from); vm.expectRevert(abi.encodeWithSignature('BucketBankruptcyBlock()')); - _pool.addQuoteToken(amount, index); + _pool.addQuoteToken(amount, index, type(uint256).max); } function _assertAddLiquidityAtIndex0Revert( @@ -720,7 +720,7 @@ abstract contract DSTestPlus is Test, IPoolEvents { ) internal { changePrank(from); vm.expectRevert(IPoolErrors.InvalidIndex.selector); - _pool.addQuoteToken(amount, 0); + _pool.addQuoteToken(amount, 0, type(uint256).max); } function _assertAddLiquidityDustRevert( @@ -730,7 +730,18 @@ abstract contract DSTestPlus is Test, IPoolEvents { ) internal { changePrank(from); vm.expectRevert(IPoolErrors.DustAmountNotExceeded.selector); - _pool.addQuoteToken(amount, index); + _pool.addQuoteToken(amount, index, type(uint256).max); + } + + function _assertAddLiquidityExpiredRevert( + address from, + uint256 amount, + uint256 index, + uint256 expiry + ) internal { + changePrank(from); + vm.expectRevert(IPoolErrors.TransactionExpired.selector); + _pool.addQuoteToken(amount, index, expiry); } function _assertArbTakeNoAuction( @@ -1100,7 +1111,7 @@ abstract contract DSTestPlus is Test, IPoolEvents { ) internal { changePrank(from); vm.expectRevert(abi.encodeWithSignature('BucketBankruptcyBlock()')); - _pool.moveQuoteToken(amount, fromIndex, toIndex); + _pool.moveQuoteToken(amount, fromIndex, toIndex, type(uint256).max); } function _assertMoveLiquidityLupBelowHtpRevert( @@ -1111,7 +1122,19 @@ abstract contract DSTestPlus is Test, IPoolEvents { ) internal { changePrank(from); vm.expectRevert(IPoolErrors.LUPBelowHTP.selector); - _pool.moveQuoteToken(amount, fromIndex, toIndex); + _pool.moveQuoteToken(amount, fromIndex, toIndex, type(uint256).max); + } + + function _assertMoveLiquidityExpiredRevert( + address from, + uint256 amount, + uint256 fromIndex, + uint256 toIndex, + uint256 expiry + ) internal { + changePrank(from); + vm.expectRevert(IPoolErrors.TransactionExpired.selector); + _pool.moveQuoteToken(amount, fromIndex, toIndex, expiry); } function _assertMoveLiquidityDustRevert( @@ -1122,7 +1145,7 @@ abstract contract DSTestPlus is Test, IPoolEvents { ) internal { changePrank(from); vm.expectRevert(IPoolErrors.DustAmountNotExceeded.selector); - _pool.moveQuoteToken(amount, fromIndex, toIndex); + _pool.moveQuoteToken(amount, fromIndex, toIndex, type(uint256).max); } function _assertMoveLiquidityToSamePriceRevert( @@ -1133,7 +1156,7 @@ abstract contract DSTestPlus is Test, IPoolEvents { ) internal { changePrank(from); vm.expectRevert(IPoolErrors.MoveToSamePrice.selector); - _pool.moveQuoteToken(amount, fromIndex, toIndex); + _pool.moveQuoteToken(amount, fromIndex, toIndex, type(uint256).max); } function _assertMoveLiquidityToIndex0Revert( @@ -1143,7 +1166,7 @@ abstract contract DSTestPlus is Test, IPoolEvents { ) internal { changePrank(from); vm.expectRevert(IPoolErrors.InvalidIndex.selector); - _pool.moveQuoteToken(amount, fromIndex, 0); + _pool.moveQuoteToken(amount, fromIndex, 0, type(uint256).max); } function _assertMoveDepositLockedByAuctionDebtRevert( @@ -1154,7 +1177,7 @@ abstract contract DSTestPlus is Test, IPoolEvents { ) internal { changePrank(from); vm.expectRevert(IPoolErrors.RemoveDepositLockedByAuctionDebt.selector); - _pool.moveQuoteToken(amount, fromIndex, toIndex); + _pool.moveQuoteToken(amount, fromIndex, toIndex, type(uint256).max); } function _assertTakeAuctionInCooldownRevert( @@ -1253,6 +1276,14 @@ abstract contract DSTestPlus is Test, IPoolEvents { lup_ = _priceAt(lupIndex); } + function setRandomSeed(uint256 seed) public { + _nonce = seed; + } + + function getNextNonce() public returns (uint256) { + return _nonce == type(uint256).max ? 0 : ++_nonce; + } + function randomInRange(uint256 min, uint256 max) public returns (uint256) { return randomInRange(min, max, false); } @@ -1260,23 +1291,19 @@ abstract contract DSTestPlus is Test, IPoolEvents { function randomInRange(uint256 min, uint256 max, bool nonZero) public returns (uint256) { if (max == 0 && nonZero) return 1; else if (max == min) return max; - uint256 rand = uint(keccak256(abi.encodePacked(block.timestamp, msg.sender, _nonce))) % (max - min + 1) + min; - _nonce++; - return rand; + return uint(keccak256(abi.encodePacked(msg.sender, getNextNonce()))) % (max - min + 1) + min; } // returns a random index between 1 and 7388 function _randomIndex() internal returns (uint256 index_) { // calculate a random index between 1 and 7388 - index_ = 1 + uint256(keccak256(abi.encodePacked(block.number, block.difficulty))) % 7387; - vm.roll(block.number + 1); // advance block to ensure that the index price is different + index_ = 1 + uint256(keccak256(abi.encodePacked(msg.sender, getNextNonce()))) % 7387; } // returns a random index between 1 and a given maximum // used for testing in NFT pools where higher indexes (and lower prices) would require so many NFTs that gas and memory limits would be exceeded function _randomIndexWithMinimumPrice(uint256 minimumPrice_) internal returns (uint256 index_) { - index_ = 1 + uint256(keccak256(abi.encodePacked(block.number, block.difficulty))) % minimumPrice_; - vm.roll(block.number + 1); // advance block to ensure that the index price is different + index_ = 1 + uint256(keccak256(abi.encodePacked(msg.sender, getNextNonce()))) % minimumPrice_; } // find the bucket index in array corresponding to the highest bucket price diff --git a/tests/forge/utils/FenwickTreeInstance.sol b/tests/forge/utils/FenwickTreeInstance.sol index 7845d6116..784d74fea 100644 --- a/tests/forge/utils/FenwickTreeInstance.sol +++ b/tests/forge/utils/FenwickTreeInstance.sol @@ -68,42 +68,83 @@ contract FenwickTreeInstance is DSTestPlus { * @notice fills fenwick tree with fuzzed values and tests additions. */ function fuzzyFill( - uint256 insertions_, - uint256 amount_, - bool trackInserts) - external { - + uint256 insertions_, // number of insertions to perform + uint256 amount_, // total amount to insert + uint256 seed_, // seed for psuedorandom number generator + bool trackInserts + ) external { uint256 i; uint256 amount; + uint256 cumulativeAmount; // Calculate total insertions - uint256 insertsDec = bound(insertions_, 1000, 2000); + insertions_ = bound(insertions_, 1000, 2000); // Calculate total amount to insert - uint256 totalAmount = bound(amount_, 1 * 1e18, 9_000_000_000_000_000 * 1e18); - uint256 totalAmountDec = totalAmount; + amount_ = bound(amount_, 1 * 1e18, 9_000_000_000_000_000 * 1e18); + // Initialize and print seed for randomness + setRandomSeed(bound(seed_, 0, type(uint256).max - 1)); - while (totalAmountDec > 0 && insertsDec > 0) { + while (amount_ > 0 && insertions_ > 0) { // Insert at random index i = randomInRange(1, MAX_FENWICK_INDEX); // If last iteration, insert remaining - amount = insertsDec == 1 ? totalAmountDec : (totalAmountDec % insertsDec) * randomInRange(1_000, 1 * 1e10, true); + amount = insertions_ == 1 ? amount_ : (amount_ % insertions_) * randomInRange(1_000, 1 * 1e10, true); // Update values add(i, amount); - totalAmountDec -= amount; - insertsDec -= 1; + amount_ -= amount; + insertions_ -= 1; + cumulativeAmount += amount; // Verify tree sum - assertEq(deposits.treeSum(), totalAmount - totalAmountDec); + assertEq(deposits.treeSum(), cumulativeAmount); if (trackInserts) inserts.push(i); } - assertEq(deposits.treeSum(), totalAmount); + assertEq(deposits.treeSum(), cumulativeAmount); } -} + /** + * @notice fills fenwick tree with deterministic values and tests additions. + */ + function nonFuzzyFill( + uint256 insertions_, // number of insertions to perform + uint256 amount_, // total amount to insert + uint256 seed_, // seed for psuedorandom number generator + bool trackInserts + ) external { + uint256 i; + uint256 amount; + uint256 cumulativeAmount; + + // Initialize and print seed for randomness + setRandomSeed(seed_); + + while (amount_ > 0 && insertions_ > 0) { + + // Insert at random index + i = randomInRange(1, MAX_FENWICK_INDEX); + + // If last iteration, insert remaining + amount = insertions_ == 1 ? amount_ : (amount_ % insertions_) * randomInRange(1_000, 1 * 1e10, true); + + // Update values + add(i, amount); + amount_ -= amount; + insertions_ -= 1; + cumulativeAmount += amount; + + // Verify tree sum + assertEq(deposits.treeSum(), cumulativeAmount); + + if (trackInserts) inserts.push(i); + } + + assertEq(deposits.treeSum(), cumulativeAmount); + } +} diff --git a/tests/forge/utils/FlashloanBorrower.sol b/tests/forge/utils/FlashloanBorrower.sol index 55e701d8f..a23db27eb 100644 --- a/tests/forge/utils/FlashloanBorrower.sol +++ b/tests/forge/utils/FlashloanBorrower.sol @@ -1,8 +1,11 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.14; +import '@openzeppelin/contracts/token/ERC20/ERC20.sol'; + import { Token } from '../utils/Tokens.sol'; +import 'src/ERC20Pool.sol'; import 'src/interfaces/pool/IERC3156FlashBorrower.sol'; import 'src/libraries/internal/Maths.sol'; @@ -32,10 +35,30 @@ contract FlashloanBorrower is IERC3156FlashBorrower { // Example of some defi strategy which produces a fixed return contract SomeDefiStrategy { - Token public token; + ERC20 public token; + + constructor(ERC20 token_) { + token = token_; + } + + function makeMoney(uint256 amount_) external { + // step 1: take deposit from caller + token.transferFrom(msg.sender, address(this), amount_); + // step 2: earn 3.5% reward + uint256 reward = Maths.wmul(0.035 * 1e18, amount_); + // step 3: profit + token.transfer(msg.sender, amount_ + reward); + } +} - constructor(Token token_) { +// Example of some defi strategy which repays to pool +contract SomeDefiStrategyWithRepayment { + ERC20 public token; + address public pool; + + constructor(ERC20 token_, address pool_) { token = token_; + pool = pool_; } function makeMoney(uint256 amount_) external { @@ -45,5 +68,8 @@ contract SomeDefiStrategy { uint256 reward = Maths.wmul(0.035 * 1e18, amount_); // step 3: profit token.transfer(msg.sender, amount_ + reward); + + // repay amount to pool + token.transfer(pool, 1 * 1e18); } } \ No newline at end of file diff --git a/tests/forge/utils/HeapInstance.sol b/tests/forge/utils/HeapInstance.sol index 779961ef9..3cc509799 100644 --- a/tests/forge/utils/HeapInstance.sol +++ b/tests/forge/utils/HeapInstance.sol @@ -60,10 +60,10 @@ contract HeapInstance is DSTestPlus { * @notice fills Heap with fuzzed values and tests additions. */ function fuzzyFill( - uint256 inserts_, - bool trackInserts_) - external { - + uint256 inserts_, // number of insertions to perform + uint256 seed_, // seed for psuedorandom number generator + bool trackInserts_ + ) external { uint256 tp; address borrower; @@ -71,6 +71,9 @@ contract HeapInstance is DSTestPlus { uint256 totalInserts = bound(inserts_, 1000, 2000); uint256 insertsDec = totalInserts; + // Initialize and print seed for randomness + setRandomSeed(bound(seed_, 0, type(uint256).max - 1)); + while (insertsDec > 0) { // build address and TP @@ -91,4 +94,3 @@ contract HeapInstance is DSTestPlus { assertEq(_heap.loans.length - 1, totalInserts); } } -