From a5322f24ab852c00b693cf8a82df1bf7a54e18e5 Mon Sep 17 00:00:00 2001 From: sherlock-admin Date: Fri, 3 Jan 2025 19:52:06 +0000 Subject: [PATCH] Fix Review --- .../LenderCommitmentGroupShares.sol | 9 +- .../LenderCommitmentGroup_Smart.sol | 143 +- .../contracts/contracts/MarketRegistry.sol | 22 +- .../packages/contracts/contracts/TellerV2.sol | 64 +- .../contracts/mock/TellerV2SolMock.sol | 2 + .../contracts/tests/MarketRegistry_Test.sol | 2 + ...mmitmentGroup_Smart_Liquidations_Tests.sol | 14 +- .../LenderCommitmentGroup_Smart_Override.sol | 33 +- .../LenderCommitmentGroup_Smart_Test.sol | 1529 +++++++++++++++++ 9 files changed, 1745 insertions(+), 73 deletions(-) diff --git a/teller-protocol-v2-audit-2024/packages/contracts/contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroupShares.sol b/teller-protocol-v2-audit-2024/packages/contracts/contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroupShares.sol index 17f450b..ed0052f 100644 --- a/teller-protocol-v2-audit-2024/packages/contracts/contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroupShares.sol +++ b/teller-protocol-v2-audit-2024/packages/contracts/contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroupShares.sol @@ -63,10 +63,13 @@ contract LenderCommitmentGroupShares is ERC20, Ownable { uint256 amount ) internal override { + if (amount > 0) { + //reset prepared + poolSharesPreparedToWithdrawForLender[from] = 0; + poolSharesPreparedTimestamp[from] = block.timestamp; + + } - //reset prepared - poolSharesPreparedToWithdrawForLender[from] = 0; - poolSharesPreparedTimestamp[from] = block.timestamp; } diff --git a/teller-protocol-v2-audit-2024/packages/contracts/contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Smart.sol b/teller-protocol-v2-audit-2024/packages/contracts/contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Smart.sol index 5baef9b..b9ba9d2 100644 --- a/teller-protocol-v2-audit-2024/packages/contracts/contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Smart.sol +++ b/teller-protocol-v2-audit-2024/packages/contracts/contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Smart.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; + // Contracts import "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; @@ -123,6 +124,7 @@ contract LenderCommitmentGroup_Smart is uint256 public totalPrincipalTokensLended; uint256 public totalPrincipalTokensRepaid; //subtract this and the above to find total principal tokens outstanding for loans + uint256 public excessivePrincipalTokensRepaid; uint256 public totalInterestCollected; @@ -142,6 +144,7 @@ contract LenderCommitmentGroup_Smart is //mapping(address => uint256) public principalTokensCommittedByLender; mapping(uint256 => bool) public activeBids; + mapping(uint256 => uint256) public activeBidsAmountDueRemaining; //this excludes interest // maybe it is possible to get rid of this storage slot and calculate it from totalPrincipalTokensRepaid, totalPrincipalTokensLended @@ -434,8 +437,15 @@ contract LenderCommitmentGroup_Smart is { int256 poolTotalEstimatedValueSigned = int256(totalPrincipalTokensCommitted) + //+ int256( totalPrincipalTokensRepaid ) //cant really incorporate because needs totalPrincipalTokensLended to help balance it + + int256(totalInterestCollected) + int256(tokenDifferenceFromLiquidations) - - int256(totalPrincipalTokensWithdrawn); + + int256( excessivePrincipalTokensRepaid ) + - int256( totalPrincipalTokensWithdrawn ) + //- int256( totalPrincipalTokensLended ) //amount borrowed -- should not be incorporated as it does not really affect net value + ; + + //if the poolTotalEstimatedValue_ is less than 0, we treat it as 0. poolTotalEstimatedValue_ = poolTotalEstimatedValueSigned > int256(0) @@ -553,7 +563,7 @@ contract LenderCommitmentGroup_Smart is "Insufficient Borrower Collateral" ); - principalToken.approve(address(TELLER_V2), _principalAmount); + principalToken.safeApprove(address(TELLER_V2), _principalAmount); //do not have to override msg.sender as this contract is the lender ! _acceptBidWithRepaymentListener(_bidId); @@ -561,6 +571,7 @@ contract LenderCommitmentGroup_Smart is totalPrincipalTokensLended += _principalAmount; activeBids[_bidId] = true; //bool for now + activeBidsAmountDueRemaining[_bidId] = _principalAmount; emit BorrowerAcceptedFunds( @@ -661,7 +672,11 @@ contract LenderCommitmentGroup_Smart is //use original principal amount as amountDue - uint256 amountDue = _getAmountOwedForBid(_bidId); + uint256 loanTotalPrincipalAmount = _getLoanTotalPrincipalAmount(_bidId); //only used for the auction delta amount + + (uint256 principalDue,uint256 interestDue) = _getAmountOwedForBid(_bidId); //this is the base amount that must be repaid by the liquidator + + // uint256 principalAmountAlreadyRepaid = loanTotalPrincipalAmount - principalDue; uint256 loanDefaultedTimeStamp = ITellerV2(TELLER_V2) @@ -673,10 +688,11 @@ contract LenderCommitmentGroup_Smart is ); int256 minAmountDifference = getMinimumAmountDifferenceToCloseDefaultedLoan( - amountDue, + loanTotalPrincipalAmount, loanDefaultedOrUnpausedAtTimeStamp ); + require( _tokenAmountDifference >= minAmountDifference, "Insufficient tokenAmountDifference" @@ -687,7 +703,8 @@ contract LenderCommitmentGroup_Smart is //this is used when the collateral value is higher than the principal (rare) //the loan will be completely made whole and our contract gets extra funds too uint256 tokensToTakeFromSender = abs(minAmountDifference); - + + uint256 liquidationProtocolFee = Math.mulDiv( tokensToTakeFromSender , @@ -699,18 +716,22 @@ contract LenderCommitmentGroup_Smart is IERC20(principalToken).safeTransferFrom( msg.sender, address(this), - amountDue + tokensToTakeFromSender - liquidationProtocolFee + principalDue + tokensToTakeFromSender - liquidationProtocolFee ); address protocolFeeRecipient = ITellerV2(address(TELLER_V2)).getProtocolFeeRecipient(); - IERC20(principalToken).safeTransferFrom( - msg.sender, - address(protocolFeeRecipient), - liquidationProtocolFee - ); + if (liquidationProtocolFee > 0) { + IERC20(principalToken).safeTransferFrom( + msg.sender, + address(protocolFeeRecipient), + liquidationProtocolFee + ); + } + + totalPrincipalTokensRepaid += principalDue; - totalPrincipalTokensRepaid += amountDue; + // tokenDifferenceFromLiquidations += int256(principalAmountAlreadyRepaid); //this helps us more correctly calculate the shortfall tokenDifferenceFromLiquidations += int256(tokensToTakeFromSender - liquidationProtocolFee ); @@ -719,21 +740,43 @@ contract LenderCommitmentGroup_Smart is uint256 tokensToGiveToSender = abs(minAmountDifference); - - IERC20(principalToken).safeTransferFrom( - msg.sender, - address(this), - amountDue - tokensToGiveToSender - ); - totalPrincipalTokensRepaid += amountDue; + + //dont stipend/refund more than principalDue base + if (tokensToGiveToSender > principalDue) { + tokensToGiveToSender = principalDue; + } + + uint256 netAmountDue = principalDue - tokensToGiveToSender ; + + + if (netAmountDue > 0) { + IERC20(principalToken).safeTransferFrom( + msg.sender, + address(this), + netAmountDue //principalDue - tokensToGiveToSender + ); + } + + totalPrincipalTokensRepaid += principalDue; //this will make tokenDifference go more negative - tokenDifferenceFromLiquidations -= int256(tokensToGiveToSender); + + //this is the shortfall + // tokenDifferenceFromLiquidations += int256(principalAmountAlreadyRepaid);//this helps us more correctly calculate the shortfall + // tokenDifferenceFromLiquidations -= int256(tokensToGiveToSender); + + // uint256 shortfallNet = principalDue < tokensToGiveToSender ? tokensToGiveToSender - principalDue : 0; + + tokenDifferenceFromLiquidations -= int256(tokensToGiveToSender); + } + //this will effectively 'forfeit' tokens from this contract equal to ... the amount (principal) that has not been repaid ! principalDue + + //this will give collateral to the caller ITellerV2(TELLER_V2).lenderCloseLoanWithRecipient(_bidId, msg.sender); @@ -741,12 +784,13 @@ contract LenderCommitmentGroup_Smart is emit DefaultedLoanLiquidated( _bidId, msg.sender, - amountDue, + principalDue, _tokenAmountDifference ); } + function getLastUnpausedAt() public view returns (uint256) { @@ -766,18 +810,29 @@ contract LenderCommitmentGroup_Smart is lastUnpausedAt = block.timestamp; } + function _getLoanTotalPrincipalAmount(uint256 _bidId ) + internal + view + virtual + returns (uint256 principalAmount) + { + (,,,, principalAmount, , , ) + = ITellerV2(TELLER_V2).getLoanSummary(_bidId); + + + } + function _getAmountOwedForBid(uint256 _bidId ) internal view virtual - returns (uint256 amountDue) + returns (uint256 principal,uint256 interest) { - (,,,, amountDue, , , ) - = ITellerV2(TELLER_V2).getLoanSummary(_bidId); + Payment memory owed = ITellerV2(TELLER_V2).calculateAmountOwed(_bidId, block.timestamp ); - + return (owed.principal, owed.interest) ; } @@ -919,21 +974,39 @@ contract LenderCommitmentGroup_Smart is /* - This callback occurs when a TellerV2 repayment happens or when a TellerV2 liquidate happens - - lenderCloseLoan does not trigger a repayLoanCallback - - It is important that only teller loans FOR THIS POOL can call this ! - */ + @dev This callback occurs when a TellerV2 repayment happens or when a TellerV2 liquidate happens + @dev lenderCloseLoan does not trigger a repayLoanCallback + @dev It is important that only teller loans for this specific pool can call this + @dev It is important that this function does not revert even if paused since repayments can occur in this case + */ function repayLoanCallback( uint256 _bidId, address repayer, uint256 principalAmount, uint256 interestAmount - ) external onlyTellerV2 whenForwarderNotPaused whenNotPaused bidIsActiveForGroup(_bidId) { - totalPrincipalTokensRepaid += principalAmount; + ) external onlyTellerV2 bidIsActiveForGroup(_bidId) { + + uint256 amountDueRemaining = activeBidsAmountDueRemaining[_bidId]; + + + uint256 principalAmountAppliedToAmountDueRemaining = principalAmount < amountDueRemaining ? + principalAmount : amountDueRemaining; + + + //should never fail due to the above . + activeBidsAmountDueRemaining[_bidId] -= principalAmountAppliedToAmountDueRemaining; + + + totalPrincipalTokensRepaid += principalAmountAppliedToAmountDueRemaining; totalInterestCollected += interestAmount; + + uint256 excessiveRepaymentAmount = principalAmount < amountDueRemaining ? + 0 : (principalAmount - amountDueRemaining); + + excessivePrincipalTokensRepaid += excessiveRepaymentAmount; + + emit LoanRepaid( _bidId, repayer, @@ -967,6 +1040,10 @@ contract LenderCommitmentGroup_Smart is view returns (uint256) { + if (totalPrincipalTokensRepaid > totalPrincipalTokensLended) { + return 0; + } + return totalPrincipalTokensLended - totalPrincipalTokensRepaid; } diff --git a/teller-protocol-v2-audit-2024/packages/contracts/contracts/MarketRegistry.sol b/teller-protocol-v2-audit-2024/packages/contracts/contracts/MarketRegistry.sol index 0c320d2..7e07926 100644 --- a/teller-protocol-v2-audit-2024/packages/contracts/contracts/MarketRegistry.sol +++ b/teller-protocol-v2-audit-2024/packages/contracts/contracts/MarketRegistry.sol @@ -329,7 +329,7 @@ contract MarketRegistry is * @notice Removes a borrower from a market via delegated revocation. * @dev See {_revokeStakeholderViaDelegation}. */ - function revokeLender( + /* function revokeLender( uint256 _marketId, address _lenderAddress, uint8 _v, @@ -344,7 +344,7 @@ contract MarketRegistry is _r, _s ); - } + } */ /** * @notice Allows a lender to voluntarily leave a market. @@ -409,7 +409,7 @@ contract MarketRegistry is * @notice Removes a borrower from a market via delegated revocation. * @dev See {_revokeStakeholderViaDelegation}. */ - function revokeBorrower( + /* function revokeBorrower( uint256 _marketId, address _borrowerAddress, uint8 _v, @@ -424,7 +424,7 @@ contract MarketRegistry is _r, _s ); - } + }*/ /** * @notice Allows a borrower to voluntarily leave a market. @@ -1180,16 +1180,8 @@ contract MarketRegistry is // tellerAS.revoke(uuid); } - /** - * @notice Removes a stakeholder from an market via delegated revocation. - * @param _marketId The market ID to remove the borrower from. - * @param _stakeholderAddress The address of the borrower to remove from the market. - * @param _isLender Boolean indicating if the stakeholder is a lender. Otherwise it is a borrower. - * @param _v Signature value - * @param _r Signature value - * @param _s Signature value - */ - function _revokeStakeholderViaDelegation( + + /* function _revokeStakeholderViaDelegation( uint256 _marketId, address _stakeholderAddress, bool _isLender, @@ -1205,7 +1197,7 @@ contract MarketRegistry is // NOTE: Disabling the call to revoke the attestation on EAS contracts // address attestor = markets[_marketId].owner; // tellerAS.revokeByDelegation(uuid, attestor, _v, _r, _s); - } + } */ /** * @notice Removes a stakeholder (borrower/lender) from a market. diff --git a/teller-protocol-v2-audit-2024/packages/contracts/contracts/TellerV2.sol b/teller-protocol-v2-audit-2024/packages/contracts/contracts/TellerV2.sol index 1d19469..fca3647 100644 --- a/teller-protocol-v2-audit-2024/packages/contracts/contracts/TellerV2.sol +++ b/teller-protocol-v2-audit-2024/packages/contracts/contracts/TellerV2.sol @@ -862,13 +862,16 @@ contract TellerV2 is emit LoanRepayment(_bidId); } - _sendOrEscrowFunds(_bidId, _payment); //send or escrow the funds + // update our mappings bid.loanDetails.totalRepaid.principal += _payment.principal; bid.loanDetails.totalRepaid.interest += _payment.interest; bid.loanDetails.lastRepaidTimestamp = uint32(block.timestamp); + //perform this after state change to mitigate re-entrancy + _sendOrEscrowFunds(_bidId, _payment); //send or escrow the funds + // If the loan is paid in full and has a mark, we should update the current reputation if (mark != RepMark.Good) { reputationManager.updateAccountReputation(bid.borrower, _bidId); @@ -933,23 +936,58 @@ contract TellerV2 is } - address loanRepaymentListener = repaymentListenerForBid[_bidId]; + address loanRepaymentListener = repaymentListenerForBid[_bidId]; if (loanRepaymentListener != address(0)) { - require(gasleft() >= 80000, "NR gas"); //fixes the 63/64 remaining issue - try - ILoanRepaymentListener(loanRepaymentListener).repayLoanCallback{ - gas: 80000 - }( //limit gas costs to prevent lender preventing repayments - _bidId, - _msgSenderForMarket(bid.marketplaceId), - _payment.principal, - _payment.interest - ) - {} catch {} + + + + + //make sure the external call will not fail due to out-of-gas + require(gasleft() >= 80000, "NR gas"); //fixes the 63/64 remaining issue + + bool repayCallbackSucccess = safeRepayLoanCallback( + loanRepaymentListener, + _bidId, + _msgSenderForMarket(bid.marketplaceId), + _payment.principal, + _payment.interest + ); + + } } + + function safeRepayLoanCallback( + address _loanRepaymentListener, + uint256 _bidId, + address _sender, + uint256 _principal, + uint256 _interest + ) internal virtual returns (bool) { + + //The EVM will only forward 63/64 of the remaining gas to the external call to _loanRepaymentListener. + + ( bool callSuccess, bytes memory callReturnData ) = ExcessivelySafeCall.excessivelySafeCall( + address(_loanRepaymentListener), + 80000, //max gas + 0, //value (eth) to send in call + 1000, //max return data size + abi.encodeWithSelector( + ILoanRepaymentListener + .repayLoanCallback + .selector, + _bidId, + _sender, + _principal, + _interest + ) + ); + + + return callSuccess ; + } /* A try/catch pattern for safeTransferERC20 that helps support standard ERC20 tokens and non-standard ones like USDT diff --git a/teller-protocol-v2-audit-2024/packages/contracts/contracts/mock/TellerV2SolMock.sol b/teller-protocol-v2-audit-2024/packages/contracts/contracts/mock/TellerV2SolMock.sol index 8dcce0b..436e769 100644 --- a/teller-protocol-v2-audit-2024/packages/contracts/contracts/mock/TellerV2SolMock.sol +++ b/teller-protocol-v2-audit-2024/packages/contracts/contracts/mock/TellerV2SolMock.sol @@ -193,6 +193,8 @@ ILoanRepaymentCallbacks ); } + + /* * @notice Calculates the minimum payment amount due for a loan. * @param _bidId The id of the loan bid to get the payment amount for. diff --git a/teller-protocol-v2-audit-2024/packages/contracts/tests/MarketRegistry_Test.sol b/teller-protocol-v2-audit-2024/packages/contracts/tests/MarketRegistry_Test.sol index f7ca920..dfd881d 100644 --- a/teller-protocol-v2-audit-2024/packages/contracts/tests/MarketRegistry_Test.sol +++ b/teller-protocol-v2-audit-2024/packages/contracts/tests/MarketRegistry_Test.sol @@ -455,6 +455,7 @@ FNDA:0,MarketRegistry._attestStakeholderViaDelegation marketRegistry.revokeStakeholder(marketId, address(lender), isLender); } +/* function test_revokeLenderViaDelegation() public { marketRegistry.revokeLender(marketId, address(lender), v, r, s); @@ -474,6 +475,7 @@ FNDA:0,MarketRegistry._attestStakeholderViaDelegation "Revoke stakeholder verification was not called" ); } + */ function test_revokeStakeholderVerification() public { bool isLender = true; diff --git a/teller-protocol-v2-audit-2024/packages/contracts/tests/SmartCommitmentForwarder/LenderCommitmentGroup_Smart_Liquidations_Tests.sol b/teller-protocol-v2-audit-2024/packages/contracts/tests/SmartCommitmentForwarder/LenderCommitmentGroup_Smart_Liquidations_Tests.sol index 550f9df..4f58598 100644 --- a/teller-protocol-v2-audit-2024/packages/contracts/tests/SmartCommitmentForwarder/LenderCommitmentGroup_Smart_Liquidations_Tests.sol +++ b/teller-protocol-v2-audit-2024/packages/contracts/tests/SmartCommitmentForwarder/LenderCommitmentGroup_Smart_Liquidations_Tests.sol @@ -236,7 +236,8 @@ contract LenderCommitmentGroup_Smart_Test is Testable { uint256 bidId = 0; - lenderCommitmentGroupSmart.set_mockAmountOwedForBid(amountOwed); + lenderCommitmentGroupSmart.set_mockLoanTotalPrincipalAmount(amountOwed); + lenderCommitmentGroupSmart.set_mockAmountOwedForBid(amountOwed, 0); @@ -291,7 +292,8 @@ contract LenderCommitmentGroup_Smart_Test is Testable { _tellerV2.setMockProtocolFeeRecipient( address(lenderCommitmentGroupSmart) ); - lenderCommitmentGroupSmart.set_mockAmountOwedForBid(amountOwed); + lenderCommitmentGroupSmart.set_mockLoanTotalPrincipalAmount(amountOwed); + lenderCommitmentGroupSmart.set_mockAmountOwedForBid(amountOwed, 0); //time has advanced enough to now have a 50 percent discount s @@ -347,7 +349,9 @@ function test_liquidateDefaultedLoanWithIncentive_increments_amount_repaid_A() p lenderCommitmentGroupSmart.set_totalPrincipalTokensCommitted(originalTotalPrincipalTokensCommitted); - lenderCommitmentGroupSmart.set_mockAmountOwedForBid(amountOwed); + lenderCommitmentGroupSmart.set_mockLoanTotalPrincipalAmount(amountOwed); + lenderCommitmentGroupSmart.set_mockAmountOwedForBid(amountOwed, 0); + _tellerV2.setMockProtocolFeeRecipient( address(lenderCommitmentGroupSmart) ); @@ -420,8 +424,8 @@ function test_liquidateDefaultedLoanWithIncentive_increments_amount_repaid_A() p _tellerV2.setMockOwner( address(lenderCommitmentGroupSmart) ); _tellerV2.setMockProtocolFeeRecipient( address(lenderCommitmentGroupSmart) ); - lenderCommitmentGroupSmart.set_mockAmountOwedForBid(amountOwed); - + lenderCommitmentGroupSmart.set_mockLoanTotalPrincipalAmount(amountOwed); + lenderCommitmentGroupSmart.set_mockAmountOwedForBid(amountOwed, 0); //time has advanced enough to now have a 50 percent discount s vm.warp(1000); //loanDefaultedTimeStamp ? diff --git a/teller-protocol-v2-audit-2024/packages/contracts/tests/SmartCommitmentForwarder/LenderCommitmentGroup_Smart_Override.sol b/teller-protocol-v2-audit-2024/packages/contracts/tests/SmartCommitmentForwarder/LenderCommitmentGroup_Smart_Override.sol index 56355c7..86d17a5 100644 --- a/teller-protocol-v2-audit-2024/packages/contracts/tests/SmartCommitmentForwarder/LenderCommitmentGroup_Smart_Override.sol +++ b/teller-protocol-v2-audit-2024/packages/contracts/tests/SmartCommitmentForwarder/LenderCommitmentGroup_Smart_Override.sol @@ -16,7 +16,10 @@ contract LenderCommitmentGroup_Smart_Override is LenderCommitmentGroup_Smart { uint256 mockSharesExchangeRate; int256 mockMinimumAmountDifferenceToCloseDefaultedLoan; - uint256 mockAmountOwed; + uint256 mockLoanTotalPrincipalAmount; + + uint256 mockAmountOwedPrincipal; + uint256 mockAmountOwedInterest; address mockToken0; address mockToken1; @@ -71,15 +74,37 @@ contract LenderCommitmentGroup_Smart_Override is LenderCommitmentGroup_Smart { } function _getAmountOwedForBid(uint256 _bidId ) + internal override view returns (uint256, uint256){ + return (mockAmountOwedPrincipal,mockAmountOwedInterest ); + + } + + function set_mockAmountOwedForBid(uint256 _principal,uint256 _interest) public { + mockAmountOwedPrincipal = _principal; + mockAmountOwedInterest = _interest; + } + + + + function _getLoanTotalPrincipalAmount(uint256 _bidId ) internal override view returns (uint256){ - return mockAmountOwed; + return mockLoanTotalPrincipalAmount; } + function set_mockLoanTotalPrincipalAmount(uint256 _principal) public { + mockLoanTotalPrincipalAmount = _principal; + + } - function set_mockAmountOwedForBid(uint256 _amt) public { - mockAmountOwed = _amt; + + function set_mockActiveBidsAmountDueRemaining(uint256 _bidId, uint256 _amount) public { + + activeBidsAmountDueRemaining[_bidId] = _amount ; } + function set_totalPrincipalTokensLended(uint256 _mockAmt) public { + totalPrincipalTokensLended = _mockAmt; + } function set_totalPrincipalTokensRepaid(uint256 _mockAmt) public { totalPrincipalTokensRepaid = _mockAmt; diff --git a/teller-protocol-v2-audit-2024/packages/contracts/tests/SmartCommitmentForwarder/LenderCommitmentGroup_Smart_Test.sol b/teller-protocol-v2-audit-2024/packages/contracts/tests/SmartCommitmentForwarder/LenderCommitmentGroup_Smart_Test.sol index 0e4812d..c05128b 100644 --- a/teller-protocol-v2-audit-2024/packages/contracts/tests/SmartCommitmentForwarder/LenderCommitmentGroup_Smart_Test.sol +++ b/teller-protocol-v2-audit-2024/packages/contracts/tests/SmartCommitmentForwarder/LenderCommitmentGroup_Smart_Test.sol @@ -92,6 +92,9 @@ contract LenderCommitmentGroup_Smart_Test is Testable { collateralToken.transfer(address(borrower), 1e18); + principalToken.transfer(address(liquidator), 1e18); + + _uniswapV3Pool.set_mockToken0(address(principalToken)); _uniswapV3Pool.set_mockToken1(address(collateralToken)); @@ -896,6 +899,1532 @@ contract LenderCommitmentGroup_Smart_Test is Testable { } + + + function test_liquidation_bid_not_active() public { + initialize_group_contract(); + + + vm.warp(1e10); + + uint256 marketId = 0; + uint256 principalAmount = 100; + uint32 loanDuration = 500000; + uint16 interestRate = 50; + + + + + // submit bid + uint256 bidId = TellerV2SolMock(_tellerV2).submitBid( + address(principalToken), + marketId, + principalAmount, + loanDuration, + interestRate, + "", + address(borrower) + ); + + + vm.prank(address(lender)); + principalToken.approve(address(_tellerV2), 1000000); + + vm.prank(address(lender)); + TellerV2SolMock(_tellerV2).lenderAcceptBid( + bidId + ); + //accept bid + + + vm.warp(1e20); + + + int256 tokenAmountDifference = 10000; + + vm.expectRevert("Bid is not active for group"); + lenderCommitmentGroupSmart.liquidateDefaultedLoanWithIncentive( + + bidId, + tokenAmountDifference + + + ); + + + + } + + + +// yarn contracts test --match-test test_liquidation_handles + function test_liquidation_handles_partially_repaid_loan_scenarioA() public { + initialize_group_contract(); + + + vm.warp(10000000000); + + uint256 marketId = 0; + uint256 principalAmount = 900; + uint32 loanDuration = 500000; + uint16 interestRate = 50; + + + + + // submit bid + uint256 bidId = TellerV2SolMock(_tellerV2).submitBid( + address(principalToken), + marketId, + principalAmount, + loanDuration, + interestRate, + "", + address(borrower) + ); + + + vm.prank(address(lender)); + principalToken.approve(address(_tellerV2), 1000000); + + vm.prank(address(lender)); + TellerV2SolMock(_tellerV2).lenderAcceptBid( + bidId + ); + + lenderCommitmentGroupSmart.set_mockBidAsActiveForGroup(bidId, true); + lenderCommitmentGroupSmart.set_mockActiveBidsAmountDueRemaining(bidId, principalAmount); + + + uint256 principalTokensCommitted = 4000; + lenderCommitmentGroupSmart.set_totalPrincipalTokensCommitted( principalTokensCommitted ); + + // do a partial repayment + + // vm.warp(100000); + + + vm.prank(address(borrower)); + principalToken.approve(address(_tellerV2), 1000000); + + + vm.prank(address(borrower)); + TellerV2SolMock(_tellerV2).repayLoan(bidId, 500); + + + uint256 repayAmount = 500; + uint256 interestAmount = 10; + + //prank the callback + vm.prank(address(_tellerV2)); + lenderCommitmentGroupSmart.repayLoanCallback( + bidId, + address(borrower), + repayAmount, + interestAmount + ); + + + vm.warp(10010000000); + + + int256 tokenAmountDifference = 200; // 10_000 + + lenderCommitmentGroupSmart.set_mockLoanTotalPrincipalAmount( principalAmount ); + + int256 tokenDifferenceToClose = 200; + + //important ! + lenderCommitmentGroupSmart.mock_setMinimumAmountDifferenceToCloseDefaultedLoan(tokenDifferenceToClose); + + vm.prank(address(liquidator)); + principalToken.approve(address(lenderCommitmentGroupSmart), 600); + + + //the liquidator sends in 1100 principal tokens + vm.prank(address(liquidator)); + //make sure accounting isnt wrong after this + lenderCommitmentGroupSmart.liquidateDefaultedLoanWithIncentive( + + bidId, + tokenAmountDifference + + + ); + + + uint256 totalPrincipalTokensRepaid = lenderCommitmentGroupSmart.totalPrincipalTokensRepaid(); + + console.log("totalPrincipalTokensRepaid") ; + console.log(totalPrincipalTokensRepaid) ; + + int256 tokenDifferenceFromLiquidations = lenderCommitmentGroupSmart.getTokenDifferenceFromLiquidations(); + + console.log("tokenDifferenceFromLiquidations") ; + console.logInt(tokenDifferenceFromLiquidations) ; + + + + + uint256 originalLoanPrincipalUnpaid = 900 - 500; + int256 netLiquidatorPayment = 900 - 500 + 200 ; // liq actually ends up paying 600 (400 + 200 ) + + + + uint256 poolTotalEstimatedValue = lenderCommitmentGroupSmart.getPoolTotalEstimatedValue(); + + console.log("poolTotalEstimatedValue") ; + console.log(poolTotalEstimatedValue) ; + + + int256 expectedPoolTotalValue = int256(principalTokensCommitted) + netLiquidatorPayment - int256(originalLoanPrincipalUnpaid) + int256(interestAmount); //where does this come from + + + assertEq(int256( poolTotalEstimatedValue), expectedPoolTotalValue); + + + + } + + + function test_liquidation_handles_partially_repaid_loan_scenarioB() public { + initialize_group_contract(); + + + vm.warp(10000000000); + + uint256 marketId = 0; + uint256 principalAmount = 900; + uint32 loanDuration = 500000; + uint16 interestRate = 50; + + + + + // submit bid + uint256 bidId = TellerV2SolMock(_tellerV2).submitBid( + address(principalToken), + marketId, + principalAmount, + loanDuration, + interestRate, + "", + address(borrower) + ); + + + vm.prank(address(lender)); + principalToken.approve(address(_tellerV2), 1000000); + + vm.prank(address(lender)); + TellerV2SolMock(_tellerV2).lenderAcceptBid( + bidId + ); + + lenderCommitmentGroupSmart.set_mockBidAsActiveForGroup(bidId, true); + lenderCommitmentGroupSmart.set_mockActiveBidsAmountDueRemaining(bidId, principalAmount); + + + uint256 principalTokensCommitted = 4000; + lenderCommitmentGroupSmart.set_totalPrincipalTokensCommitted( principalTokensCommitted ); + + // do a partial repayment + + // vm.warp(100000); + + + vm.prank(address(borrower)); + principalToken.approve(address(_tellerV2), 1000000); + + + vm.prank(address(borrower)); + TellerV2SolMock(_tellerV2).repayLoan(bidId, 500); + + + uint256 repayAmount = 500; + uint256 interestAmount = 10; + + //prank the callback + vm.prank(address(_tellerV2)); + lenderCommitmentGroupSmart.repayLoanCallback( + bidId, + address(borrower), + repayAmount, + interestAmount + ); + + + lenderCommitmentGroupSmart.set_mockAmountOwedForBid( principalAmount - repayAmount, 0 ); + + + vm.warp(10010000000); + + + int256 tokenAmountDifference = -200; // 10_000 + + lenderCommitmentGroupSmart.set_mockLoanTotalPrincipalAmount( principalAmount ); + + int256 tokenDifferenceToClose = -200; + + //important ! + lenderCommitmentGroupSmart.mock_setMinimumAmountDifferenceToCloseDefaultedLoan(tokenDifferenceToClose); + + vm.prank(address(liquidator)); + principalToken.approve(address(lenderCommitmentGroupSmart), 600); + + + //the liquidator sends in 1100 principal tokens + vm.prank(address(liquidator)); + //make sure accounting isnt wrong after this + lenderCommitmentGroupSmart.liquidateDefaultedLoanWithIncentive( + + bidId, + tokenAmountDifference + + + ); + + + uint256 totalPrincipalTokensRepaid = lenderCommitmentGroupSmart.totalPrincipalTokensRepaid(); + + console.log("totalPrincipalTokensRepaid") ; + console.log(totalPrincipalTokensRepaid) ; + + int256 tokenDifferenceFromLiquidations = lenderCommitmentGroupSmart.getTokenDifferenceFromLiquidations(); + + console.log("tokenDifferenceFromLiquidations") ; + console.logInt(tokenDifferenceFromLiquidations) ; + + + + + uint256 originalLoanPrincipalUnpaid = 900 - 500; + int256 netLiquidatorPayment = 900 - 500 -200 ; // liq actually ends up paying 200 less (200 total) since tokensToGiveToSender > principalDue + + + + uint256 poolTotalEstimatedValue = lenderCommitmentGroupSmart.getPoolTotalEstimatedValue(); + + console.log("poolTotalEstimatedValue") ; + console.log(poolTotalEstimatedValue) ; + + // int256 expectedPoolValue = int256(principalTokensCommitted) + int256(interestAmount) + tokenDifferenceToClose; // compute this + + int256 expectedPoolTotalValue = int256(principalTokensCommitted) + + netLiquidatorPayment - int256(originalLoanPrincipalUnpaid) + + int256(interestAmount); //where does this come from + + + assertEq(int256( poolTotalEstimatedValue), expectedPoolTotalValue); + + + + } + + + + function test_liquidation_handles_partially_repaid_loan_scenarioB2() public { + initialize_group_contract(); + + + vm.warp(10000000000); + + uint256 marketId = 0; + uint256 principalAmount = 900; + uint32 loanDuration = 500000; + uint16 interestRate = 50; + + + + + // submit bid + uint256 bidId = TellerV2SolMock(_tellerV2).submitBid( + address(principalToken), + marketId, + principalAmount, + loanDuration, + interestRate, + "", + address(borrower) + ); + + + vm.prank(address(lender)); + principalToken.approve(address(_tellerV2), 1000000); + + vm.prank(address(lender)); + TellerV2SolMock(_tellerV2).lenderAcceptBid( + bidId + ); + + lenderCommitmentGroupSmart.set_mockBidAsActiveForGroup(bidId, true); + lenderCommitmentGroupSmart.set_mockActiveBidsAmountDueRemaining(bidId, principalAmount); + + + uint256 principalTokensCommitted = 4000; + lenderCommitmentGroupSmart.set_totalPrincipalTokensCommitted( principalTokensCommitted ); + + // do a partial repayment + + // vm.warp(100000); + + + vm.prank(address(borrower)); + principalToken.approve(address(_tellerV2), 1000000); + + + vm.prank(address(borrower)); + TellerV2SolMock(_tellerV2).repayLoan(bidId, 500); + + + uint256 repayAmount = 500; + uint256 interestAmount = 10; + + //prank the callback + vm.prank(address(_tellerV2)); + lenderCommitmentGroupSmart.repayLoanCallback( + bidId, + address(borrower), + repayAmount, + interestAmount + ); + + + lenderCommitmentGroupSmart.set_mockAmountOwedForBid( principalAmount - repayAmount, 0 ); + + + vm.warp(10010000000); + + + int256 tokenAmountDifference = -2000; // 10_000 + + lenderCommitmentGroupSmart.set_mockLoanTotalPrincipalAmount( principalAmount ); + + int256 tokenDifferenceToClose = -2000; + + //important ! + lenderCommitmentGroupSmart.mock_setMinimumAmountDifferenceToCloseDefaultedLoan(tokenDifferenceToClose); + + vm.prank(address(liquidator)); + principalToken.approve(address(lenderCommitmentGroupSmart), 600); + + + //the liquidator sends in 1100 principal tokens + vm.prank(address(liquidator)); + //make sure accounting isnt wrong after this + lenderCommitmentGroupSmart.liquidateDefaultedLoanWithIncentive( + + bidId, + tokenAmountDifference + + + ); + + + uint256 totalPrincipalTokensRepaid = lenderCommitmentGroupSmart.totalPrincipalTokensRepaid(); + + console.log("totalPrincipalTokensRepaid") ; + console.log(totalPrincipalTokensRepaid) ; + + int256 tokenDifferenceFromLiquidations = lenderCommitmentGroupSmart.getTokenDifferenceFromLiquidations(); + + console.log("tokenDifferenceFromLiquidations") ; + console.logInt(tokenDifferenceFromLiquidations) ; + + + + + uint256 originalLoanPrincipalUnpaid = 900 - 500; + int256 netLiquidatorPayment = 0 ; // liq actually ends up paying none at all since tokensToGiveToSender > principalDue + + + + uint256 poolTotalEstimatedValue = lenderCommitmentGroupSmart.getPoolTotalEstimatedValue(); + + console.log("poolTotalEstimatedValue") ; + console.log(poolTotalEstimatedValue) ; + + // int256 expectedPoolValue = int256(principalTokensCommitted) + int256(interestAmount) + tokenDifferenceToClose; // compute this + + int256 expectedPoolTotalValue = int256(principalTokensCommitted) + + netLiquidatorPayment - int256(originalLoanPrincipalUnpaid) + + int256(interestAmount); //where does this come from + + + assertEq(int256( poolTotalEstimatedValue), expectedPoolTotalValue); + + + + } + + + + function test_liquidation_handles_partially_repaid_loan_scenarioC() public { + initialize_group_contract(); + + + vm.warp(10000000000); + + uint256 marketId = 0; + uint256 principalAmount = 4000; + uint32 loanDuration = 500000; + uint16 interestRate = 50; + + + + uint256 principalTokensCommitted = 40000; + lenderCommitmentGroupSmart.set_totalPrincipalTokensCommitted( principalTokensCommitted ); + + + + + + // submit bid + uint256 bidId = TellerV2SolMock(_tellerV2).submitBid( + address(principalToken), + marketId, + principalAmount, + loanDuration, + interestRate, + "", + address(borrower) + ); + + + vm.prank(address(lender)); + principalToken.approve(address(_tellerV2), 1000000); + + vm.prank(address(lender)); + TellerV2SolMock(_tellerV2).lenderAcceptBid( + bidId + ); + + lenderCommitmentGroupSmart.set_mockBidAsActiveForGroup(bidId, true); + lenderCommitmentGroupSmart.set_mockActiveBidsAmountDueRemaining(bidId, principalAmount); + + + + // do a partial repayment + + // vm.warp(100000); + + + vm.prank(address(borrower)); + principalToken.approve(address(_tellerV2), 1000000); + + + vm.prank(address(borrower)); + TellerV2SolMock(_tellerV2).repayLoan(bidId, 510); + + + + uint256 repayAmount = 500; + uint256 interestAmount = 10 ; + + //prank the callback + vm.prank(address(_tellerV2)); + lenderCommitmentGroupSmart.repayLoanCallback( + bidId, + address(borrower), + repayAmount, + interestAmount + ); + + lenderCommitmentGroupSmart.set_mockAmountOwedForBid( principalAmount - 500, 0 ); + + + + + vm.warp(10010000000); + + + int256 tokenAmountDifference = -200 ; + + lenderCommitmentGroupSmart.set_mockLoanTotalPrincipalAmount(principalAmount); + + //important ! + lenderCommitmentGroupSmart.mock_setMinimumAmountDifferenceToCloseDefaultedLoan(-200); + + vm.prank(address(liquidator)); + principalToken.approve(address(lenderCommitmentGroupSmart), principalAmount-200); + + + //the liquidator sends in 700 principal tokens + vm.prank(address(liquidator)); + //make sure accounting isnt incorrect after this + lenderCommitmentGroupSmart.liquidateDefaultedLoanWithIncentive( + + bidId, + tokenAmountDifference + + + ); + + + uint256 totalPrincipalTokensRepaid = lenderCommitmentGroupSmart + .totalPrincipalTokensRepaid(); + + console.log("totalPrincipalTokensRepaid") ; + console.log(totalPrincipalTokensRepaid) ; + + int256 tokenDifferenceFromLiquidations = lenderCommitmentGroupSmart + .getTokenDifferenceFromLiquidations(); + + console.log("tokenDifferenceFromLiquidations") ; + console.logInt(tokenDifferenceFromLiquidations) ; + + + + + + uint256 originalLoanPrincipalUnpaid = principalAmount - repayAmount ; + + int256 netLiquidatorPayment = 4000 - 500 - 200 ; // 3500 - 200 + + + uint256 poolTotalEstimatedValue = lenderCommitmentGroupSmart.getPoolTotalEstimatedValue(); + + console.log("poolTotalEstimatedValue") ; + console.log(poolTotalEstimatedValue) ; + + + int256 expectedPoolTotalValue = int256(principalTokensCommitted) + + netLiquidatorPayment - int256(originalLoanPrincipalUnpaid) + + int256(interestAmount); //where does this come from + + + assertEq(poolTotalEstimatedValue , uint256( expectedPoolTotalValue )); + + } + + + /* + + extreme example + */ + function test_liquidation_handles_partially_repaid_loan_scenarioD() public { + initialize_group_contract(); + + + vm.warp(10000000000); + + uint256 marketId = 0; + uint256 principalAmount = 5000; + uint32 loanDuration = 500000; + uint16 interestRate = 50; + + + + uint256 principalTokensCommitted = 40000; + lenderCommitmentGroupSmart.set_totalPrincipalTokensCommitted( principalTokensCommitted ); + + + + + + // submit bid + uint256 bidId = TellerV2SolMock(_tellerV2).submitBid( + address(principalToken), + marketId, + principalAmount, + loanDuration, + interestRate, + "", + address(borrower) + ); + + + vm.prank(address(lender)); + principalToken.approve(address(_tellerV2), 1000000); + + vm.prank(address(lender)); + TellerV2SolMock(_tellerV2).lenderAcceptBid( + bidId + ); + + lenderCommitmentGroupSmart.set_mockBidAsActiveForGroup(bidId, true); + lenderCommitmentGroupSmart.set_mockActiveBidsAmountDueRemaining(bidId, principalAmount); + + + + // do a partial repayment + + // vm.warp(100000); + + + vm.prank(address(borrower)); + principalToken.approve(address(_tellerV2), 1000000); + + + + uint256 repayAmount = 4900; //repay almost the entire loan + uint256 interestAmount = 50 ; + + vm.prank(address(borrower)); + TellerV2SolMock(_tellerV2).repayLoan(bidId, repayAmount); + + + + //prank the callback + vm.prank(address(_tellerV2)); + lenderCommitmentGroupSmart.repayLoanCallback( + bidId, + address(borrower), + repayAmount, + interestAmount + ); + + + //declare what is still owed after repay + lenderCommitmentGroupSmart.set_mockAmountOwedForBid( 100, 0 ); + + + + vm.warp(10010000000); + + + int256 tokenAmountDifference = 10000; + + lenderCommitmentGroupSmart.set_mockLoanTotalPrincipalAmount( principalAmount ); + + //important ! + lenderCommitmentGroupSmart.mock_setMinimumAmountDifferenceToCloseDefaultedLoan(-200); + + vm.prank(address(liquidator)); + principalToken.approve(address(lenderCommitmentGroupSmart), principalAmount-200); + + + //the liquidator sends in 700 principal tokens + vm.prank(address(liquidator)); + //make sure accounting isnt incorrect after this + lenderCommitmentGroupSmart.liquidateDefaultedLoanWithIncentive( + + bidId, + tokenAmountDifference + + + ); + + + //this doesnt directly contribute to the pool total estimated value + uint256 totalPrincipalTokensRepaid = lenderCommitmentGroupSmart.totalPrincipalTokensRepaid(); + + console.log("totalPrincipalTokensRepaid") ; + console.log(totalPrincipalTokensRepaid) ; + + int256 tokenDifferenceFromLiquidations = lenderCommitmentGroupSmart.getTokenDifferenceFromLiquidations(); + + console.log("tokenDifferenceFromLiquidations") ; + console.logInt(tokenDifferenceFromLiquidations) ; + + + + uint256 poolTotalEstimatedValue = lenderCommitmentGroupSmart.getPoolTotalEstimatedValue(); + + console.log("poolTotalEstimatedValue") ; + console.log(poolTotalEstimatedValue) ; + + + uint256 originalLoanPrincipalUnpaid = principalAmount - repayAmount ; + int256 netLiquidatorPayment = int256( 0 ) ; // 100 + -200 + // 10000 + 50 + + + //calculated in a different way than the solidity does. More understandable to user story + int256 expectedPoolTotalValue = int256(principalTokensCommitted) + netLiquidatorPayment - int256(originalLoanPrincipalUnpaid) + int256(interestAmount); //where does this come from + + + + // pool originally has value of 40_000 + // a loan of 5000 is taken out + // a repayment is made on it for 4900 + 50 , so 100 is still owed + + // its never paid off so it goes into liquidation + // the liquidation auction completes at -200 delta + // this means that the liquidator paid 5000 - 200 = 4800 + + //this means that the pool value should be 40000 + 4800 - 100 + 50 ( ?? ) + // this is pool committed amount + liquidator payment - amount OG lender was short + interest earned + + + //ends up being 44750 + assertEq(poolTotalEstimatedValue , uint256( expectedPoolTotalValue )); + + + } + + + + function test_liquidation_handles_partially_repaid_loan_scenarioE() public { + initialize_group_contract(); + + + vm.warp(10000000000); + + uint256 marketId = 0; + uint256 principalAmount = 5000; + uint32 loanDuration = 500000; + uint16 interestRate = 50; + + + + uint256 principalTokensCommitted = 40000; + lenderCommitmentGroupSmart.set_totalPrincipalTokensCommitted( principalTokensCommitted ); + + + + + + // submit bid + uint256 bidId = TellerV2SolMock(_tellerV2).submitBid( + address(principalToken), + marketId, + principalAmount, + loanDuration, + interestRate, + "", + address(borrower) + ); + + + vm.prank(address(lender)); + principalToken.approve(address(_tellerV2), 1000000); + + vm.prank(address(lender)); + TellerV2SolMock(_tellerV2).lenderAcceptBid( + bidId + ); + + lenderCommitmentGroupSmart.set_mockBidAsActiveForGroup(bidId, true); + lenderCommitmentGroupSmart.set_mockActiveBidsAmountDueRemaining(bidId, principalAmount); + + + + // do a partial repayment + + // vm.warp(100000); + + + vm.prank(address(borrower)); + principalToken.approve(address(_tellerV2), 1000000); + + + + uint256 repayAmount = 4900; //repay almost the entire loan + uint256 interestAmount = 50 ; + + vm.prank(address(borrower)); + TellerV2SolMock(_tellerV2).repayLoan(bidId, repayAmount); + + + + //prank the callback + vm.prank(address(_tellerV2)); + lenderCommitmentGroupSmart.repayLoanCallback( + bidId, + address(borrower), + repayAmount, + interestAmount + ); + + + //declare what is still owed after repay + lenderCommitmentGroupSmart.set_mockAmountOwedForBid( 100, 0 ); + + + + vm.warp(10010000000); + + + int256 tokenAmountDifference = 200; + + lenderCommitmentGroupSmart.set_mockLoanTotalPrincipalAmount( principalAmount ); + + //important ! + lenderCommitmentGroupSmart.mock_setMinimumAmountDifferenceToCloseDefaultedLoan(200); + + vm.prank(address(liquidator)); + principalToken.approve(address(lenderCommitmentGroupSmart), principalAmount + 200); + + + //the liquidator sends in 700 principal tokens + vm.prank(address(liquidator)); + //make sure accounting isnt incorrect after this + lenderCommitmentGroupSmart.liquidateDefaultedLoanWithIncentive( + + bidId, + tokenAmountDifference + + + ); + + + //this doesnt directly contribute to the pool total estimated value + uint256 totalPrincipalTokensRepaid = lenderCommitmentGroupSmart.totalPrincipalTokensRepaid(); + + console.log("totalPrincipalTokensRepaid") ; + console.log(totalPrincipalTokensRepaid) ; + + int256 tokenDifferenceFromLiquidations = lenderCommitmentGroupSmart.getTokenDifferenceFromLiquidations(); + + console.log("tokenDifferenceFromLiquidations") ; + console.logInt(tokenDifferenceFromLiquidations) ; + + + + uint256 poolTotalEstimatedValue = lenderCommitmentGroupSmart.getPoolTotalEstimatedValue(); + + console.log("poolTotalEstimatedValue") ; + console.log(poolTotalEstimatedValue) ; + + + uint256 originalLoanPrincipalUnpaid = principalAmount - repayAmount ; + + //amount Due + 200 + int256 netLiquidatorPayment = int256( 100 + 200 ) ; // 5000 + -200 + // 10000 + 50 + + + //calculated in a different way than the solidity does. More understandable to user story + int256 expectedPoolTotalValue = int256(principalTokensCommitted) + netLiquidatorPayment - int256(originalLoanPrincipalUnpaid) + int256(interestAmount); //where does this come from + + + + // pool originally has value of 40_000 + // a loan of 5000 is taken out + // a repayment is made on it for 4900 + 50 , so 100 is still owed + + // its never paid off so it goes into liquidation + // the liquidation auction completes at -200 delta + // this means that the liquidator paid 5000 - 200 = 4800 + + //this means that the pool value should be 40000 + 4800 - 100 + 50 ( ?? ) + // this is pool committed amount + liquidator payment - amount OG lender was short + interest earned + + + //ends up being 44750 + assertEq(poolTotalEstimatedValue , uint256( expectedPoolTotalValue )); + + + } + + + + + + + function test_excessive_repay_pool_value_A() public { + initialize_group_contract(); + + + vm.warp(10000000000); + + uint256 marketId = 0; + uint256 principalAmount = 5000; + uint32 loanDuration = 500000; + uint16 interestRate = 50; + + + + uint256 principalTokensCommitted = 5000; + lenderCommitmentGroupSmart.set_totalPrincipalTokensCommitted( principalTokensCommitted ); + + + + + + // submit bid + uint256 bidId = TellerV2SolMock(_tellerV2).submitBid( + address(principalToken), + marketId, + principalAmount, + loanDuration, + interestRate, + "", + address(borrower) + ); + + + + assertEq( + lenderCommitmentGroupSmart.getPrincipalAmountAvailableToBorrow(), + 5000 + ); + + + //give the borrower some extra $$ for the test + principalToken.transfer(address(borrower), 1e18); + + + vm.prank(address(lender)); + principalToken.approve(address(_tellerV2), 5000); + + vm.prank(address(lender)); + TellerV2SolMock(_tellerV2).lenderAcceptBid( + bidId + ); + + lenderCommitmentGroupSmart.set_mockBidAsActiveForGroup(bidId, true); + lenderCommitmentGroupSmart.set_mockActiveBidsAmountDueRemaining(bidId, principalAmount); + + + //mocking what happens in acceptFunds + lenderCommitmentGroupSmart.set_totalPrincipalTokensLended(5000); + + + + assertEq( + lenderCommitmentGroupSmart.getPrincipalAmountAvailableToBorrow(), + 0, + "get principal amount available to borrow 1 " + ); + + // do a partial repayment + + // vm.warp(100000); + + + vm.prank(address(borrower)); + principalToken.approve(address(_tellerV2), 1000000); + + + + uint256 repayAmount = 14900; //repay a highly excessive amount + uint256 interestAmount = 50 ; + + vm.prank(address(borrower)); + TellerV2SolMock(_tellerV2).repayLoan(bidId, repayAmount); + + + + //prank the callback + vm.prank(address(_tellerV2)); + lenderCommitmentGroupSmart.repayLoanCallback( + bidId, + address(borrower), + repayAmount, + interestAmount + ); + + + //declare what is still owed after repay + lenderCommitmentGroupSmart.set_mockAmountOwedForBid( 100, 0 ); + + + + vm.warp(10010000000); + + + + //can now borrow what was repaid + interest , CANNOT borrow the excess repaid amt + assertEq( + lenderCommitmentGroupSmart.getPrincipalAmountAvailableToBorrow(), + 14950, + "get principal amount available to borrow 2 " + ); + + + //value of the pool DOES include excessive repaid amount . + assertEq( + lenderCommitmentGroupSmart.getPoolTotalEstimatedValue(), + 14950, + "getPoolTotalEstimatedValue 1 " + ); + + + + + + + + int256 tokenAmountDifference = 200; + + lenderCommitmentGroupSmart.set_mockLoanTotalPrincipalAmount( principalAmount ); + + //important ! + lenderCommitmentGroupSmart.mock_setMinimumAmountDifferenceToCloseDefaultedLoan(200); + + vm.prank(address(liquidator)); + principalToken.approve(address(lenderCommitmentGroupSmart), principalAmount + 200); + + + //the liquidator sends in 700 principal tokens + vm.prank(address(liquidator)); + //make sure accounting isnt incorrect after this + lenderCommitmentGroupSmart.liquidateDefaultedLoanWithIncentive( + + bidId, + tokenAmountDifference + + + ); + + + //this doesnt directly contribute to the pool total estimated value + uint256 totalPrincipalTokensRepaid = lenderCommitmentGroupSmart.totalPrincipalTokensRepaid(); + + console.log("totalPrincipalTokensRepaid") ; + console.log(totalPrincipalTokensRepaid) ; + + int256 tokenDifferenceFromLiquidations = lenderCommitmentGroupSmart.getTokenDifferenceFromLiquidations(); + + console.log("tokenDifferenceFromLiquidations") ; + console.logInt(tokenDifferenceFromLiquidations) ; + + + + uint256 poolTotalEstimatedValue = lenderCommitmentGroupSmart.getPoolTotalEstimatedValue(); + + console.log("poolTotalEstimatedValue") ; + console.log(poolTotalEstimatedValue) ; + + + int256 originalLoanPrincipalUnpaid = int256( principalAmount ) - int256( repayAmount ) ; + + // originalLoanPrincipalUnpaid = Math.max ( originalLoanPrincipalUnpaid , 0) ; + + //simulate what is done in the contract + if (originalLoanPrincipalUnpaid < 0) { + originalLoanPrincipalUnpaid= 0; + } + + + // int256 liqAmountDue = originalLoanPrincipalUnpaid ; + int256 netLiquidatorPayment = int256( originalLoanPrincipalUnpaid + 200 ) ; // 5000 + -200 + + + + + int256 excessRepaidAmount = 9900 ; + + + //calculated in a different way than the solidity does. More understandable to user story + int256 expectedPoolTotalValue = int256( + principalTokensCommitted) + + netLiquidatorPayment + + excessRepaidAmount + - int256(originalLoanPrincipalUnpaid) + + int256(interestAmount); //where does this come from + + + assertEq(poolTotalEstimatedValue , uint256( expectedPoolTotalValue )); + + + } + + + + + + function test_excessive_repay_pool_value_B() public { + initialize_group_contract(); + + + vm.warp(10000000000); + + uint256 marketId = 0; + uint256 principalAmount = 5000; + uint32 loanDuration = 500000; + uint16 interestRate = 50; + + + + uint256 principalTokensCommitted = 55000; + lenderCommitmentGroupSmart.set_totalPrincipalTokensCommitted( principalTokensCommitted ); + + + //the pool has lended and repaid a significant volume of loans + lenderCommitmentGroupSmart.set_totalPrincipalTokensLended(50000); + lenderCommitmentGroupSmart.set_totalPrincipalTokensRepaid(50000); + + + + // submit bid + uint256 bidId = TellerV2SolMock(_tellerV2).submitBid( + address(principalToken), + marketId, + principalAmount, + loanDuration, + interestRate, + "", + address(borrower) + ); + + + + assertEq( + lenderCommitmentGroupSmart.getPrincipalAmountAvailableToBorrow(), + 55000 + ); + + + //give the borrower some extra $$ for the test + principalToken.transfer(address(borrower), 1e18); + + + vm.prank(address(lender)); + principalToken.approve(address(_tellerV2), 5000); + + vm.prank(address(lender)); + TellerV2SolMock(_tellerV2).lenderAcceptBid( + bidId + ); + + lenderCommitmentGroupSmart.set_mockBidAsActiveForGroup(bidId, true); + + lenderCommitmentGroupSmart.set_mockActiveBidsAmountDueRemaining(bidId, principalAmount); + + + //mocking what happens in acceptFunds + lenderCommitmentGroupSmart.set_totalPrincipalTokensLended(55000); + + + + assertEq( + lenderCommitmentGroupSmart.getPrincipalAmountAvailableToBorrow(), + 50000, + "get principal amount available to borrow 1 " + ); + + // do a partial repayment + + // vm.warp(100000); + + + vm.prank(address(borrower)); + principalToken.approve(address(_tellerV2), 1000000); + + + + uint256 repayAmount = 14900; //repay a highly excessive amount + uint256 interestAmount = 50 ; + + vm.prank(address(borrower)); + TellerV2SolMock(_tellerV2).repayLoan(bidId, repayAmount); + + + + //prank the callback + vm.prank(address(_tellerV2)); + lenderCommitmentGroupSmart.repayLoanCallback( + bidId, + address(borrower), + repayAmount, + interestAmount + ); + + + //declare what is still owed after repay + lenderCommitmentGroupSmart.set_mockAmountOwedForBid( 100, 0 ); + + + + vm.warp(10010000000); + + + + //can now borrow what was repaid + interest , CAN borrow the excess repaid amt + assertEq( + lenderCommitmentGroupSmart.getPrincipalAmountAvailableToBorrow(), + 64950, + "get principal amount available to borrow 2 " + ); + + + //value of the pool DOE include excessive repaid amount . + assertEq( + lenderCommitmentGroupSmart.getPoolTotalEstimatedValue(), + 64950, + "getPoolTotalEstimatedValue 1 " + ); + + + + + + + + int256 tokenAmountDifference = 200; + + lenderCommitmentGroupSmart.set_mockLoanTotalPrincipalAmount( principalAmount ); + + //important ! + lenderCommitmentGroupSmart.mock_setMinimumAmountDifferenceToCloseDefaultedLoan(200); + + vm.prank(address(liquidator)); + principalToken.approve(address(lenderCommitmentGroupSmart), principalAmount + 200); + + + //the liquidator sends in 700 principal tokens + vm.prank(address(liquidator)); + //make sure accounting isnt incorrect after this + lenderCommitmentGroupSmart.liquidateDefaultedLoanWithIncentive( + + bidId, + tokenAmountDifference + + + ); + + + //this doesnt directly contribute to the pool total estimated value + uint256 totalPrincipalTokensRepaid = lenderCommitmentGroupSmart.totalPrincipalTokensRepaid(); + + console.log("totalPrincipalTokensRepaid") ; + console.log(totalPrincipalTokensRepaid) ; + + int256 tokenDifferenceFromLiquidations = lenderCommitmentGroupSmart.getTokenDifferenceFromLiquidations(); + + console.log("tokenDifferenceFromLiquidations") ; + console.logInt(tokenDifferenceFromLiquidations) ; + + + + uint256 poolTotalEstimatedValue = lenderCommitmentGroupSmart.getPoolTotalEstimatedValue(); + + console.log("poolTotalEstimatedValue") ; + console.log(poolTotalEstimatedValue) ; + + + int256 originalLoanPrincipalUnpaid = int256( principalAmount ) - int256( repayAmount ) ; + + // originalLoanPrincipalUnpaid = Math.max ( originalLoanPrincipalUnpaid , 0) ; + + //simulate what is done in the contract + if (originalLoanPrincipalUnpaid < 0) { + originalLoanPrincipalUnpaid= 0; + } + + //amount Due + 200 + //int256 liqAmountDue = originalLoanPrincipalUnpaid ; + int256 netLiquidatorPayment = int256( originalLoanPrincipalUnpaid + 200 ) ; // 5000 + -200 + // 10000 + 50 + + + int256 excessRepaidAmount = 9900 ; + + + //calculated in a different way than the solidity does. More understandable to user story + int256 expectedPoolTotalValue = int256(principalTokensCommitted) + + netLiquidatorPayment + + excessRepaidAmount + - int256(originalLoanPrincipalUnpaid) + + int256(interestAmount); //where does this come from + + + + assertEq(poolTotalEstimatedValue , uint256( expectedPoolTotalValue )); + + + } + + + + + + function test_excessive_repay_pool_value_C() public { + initialize_group_contract(); + + + vm.warp(10000000000); + + uint256 marketId = 0; + uint256 principalAmount = 5000; + uint32 loanDuration = 500000; + uint16 interestRate = 50; + + + + uint256 principalTokensCommitted = 55000; + lenderCommitmentGroupSmart.set_totalPrincipalTokensCommitted( principalTokensCommitted ); + + + + lenderCommitmentGroupSmart.set_totalPrincipalTokensLended(50000); + lenderCommitmentGroupSmart.set_totalPrincipalTokensRepaid(0); + + + + // submit bid + uint256 bidId = TellerV2SolMock(_tellerV2).submitBid( + address(principalToken), + marketId, + principalAmount, + loanDuration, + interestRate, + "", + address(borrower) + ); + + + + assertEq( + lenderCommitmentGroupSmart.getPrincipalAmountAvailableToBorrow(), + 5000 + ); + + + //give the borrower some extra $$ for the test + principalToken.transfer(address(borrower), 1e18); + + + vm.prank(address(lender)); + principalToken.approve(address(_tellerV2), 5000); + + vm.prank(address(lender)); + TellerV2SolMock(_tellerV2).lenderAcceptBid( + bidId + ); + + lenderCommitmentGroupSmart.set_mockBidAsActiveForGroup(bidId, true); + lenderCommitmentGroupSmart.set_mockActiveBidsAmountDueRemaining(bidId, principalAmount); + + + //mocking what happens in acceptFunds + lenderCommitmentGroupSmart.set_totalPrincipalTokensLended(55000); + + + + assertEq( + lenderCommitmentGroupSmart.getPrincipalAmountAvailableToBorrow(), + 0, + "get principal amount available to borrow 1 " + ); + + // do a partial repayment + + // vm.warp(100000); + + + vm.prank(address(borrower)); + principalToken.approve(address(_tellerV2), 1000000); + + + + uint256 repayAmount = 14900; //repay a highly excessive amount + uint256 interestAmount = 50 ; + + vm.prank(address(borrower)); + TellerV2SolMock(_tellerV2).repayLoan(bidId, repayAmount); + + + + //prank the callback + vm.prank(address(_tellerV2)); + lenderCommitmentGroupSmart.repayLoanCallback( + bidId, + address(borrower), + repayAmount, + interestAmount + ); + + + //declare what is still owed after repay + lenderCommitmentGroupSmart.set_mockAmountOwedForBid( 100, 0 ); + + + + vm.warp(10010000000); + + + + //can now borrow what was repaid + interest , CAN borrow the excess repaid amt + assertEq( + lenderCommitmentGroupSmart.getPrincipalAmountAvailableToBorrow(), + 14950, + "get principal amount available to borrow 2 " + ); + + + //value of the pool DOES also include excessive repaid amount . + assertEq( + lenderCommitmentGroupSmart.getPoolTotalEstimatedValue(), + 64950, + "getPoolTotalEstimatedValue 1 " + ); + + + + + + + + int256 tokenAmountDifference = 200; + + lenderCommitmentGroupSmart.set_mockLoanTotalPrincipalAmount( principalAmount ); + + //important ! + lenderCommitmentGroupSmart.mock_setMinimumAmountDifferenceToCloseDefaultedLoan(200); + + vm.prank(address(liquidator)); + principalToken.approve(address(lenderCommitmentGroupSmart), principalAmount + 200); + + + //the liquidator sends in 700 principal tokens + vm.prank(address(liquidator)); + //make sure accounting isnt incorrect after this + lenderCommitmentGroupSmart.liquidateDefaultedLoanWithIncentive( + + bidId, + tokenAmountDifference + + + ); + + + //this doesnt directly contribute to the pool total estimated value + uint256 totalPrincipalTokensRepaid = lenderCommitmentGroupSmart.totalPrincipalTokensRepaid(); + + console.log("totalPrincipalTokensRepaid") ; + console.log(totalPrincipalTokensRepaid) ; + + int256 tokenDifferenceFromLiquidations = lenderCommitmentGroupSmart.getTokenDifferenceFromLiquidations(); + + console.log("tokenDifferenceFromLiquidations") ; + console.logInt(tokenDifferenceFromLiquidations) ; + + + + uint256 poolTotalEstimatedValue = lenderCommitmentGroupSmart.getPoolTotalEstimatedValue(); + + console.log("poolTotalEstimatedValue") ; + console.log(poolTotalEstimatedValue) ; + + + int256 originalLoanPrincipalUnpaid = + int256( principalAmount ) + - int256( repayAmount ) ; + + // originalLoanPrincipalUnpaid = Math.max ( originalLoanPrincipalUnpaid , 0) ; + + //simulate what is done in the contract + if (originalLoanPrincipalUnpaid < 0) { + originalLoanPrincipalUnpaid= 0; + } + + //amount Due + 200 + + int256 netLiquidatorPayment = int256( originalLoanPrincipalUnpaid + 200 ) ; // 5000 + -200 + // 10000 + 50 + + int256 excessRepaidAmount = 9900 ; + + //calculated in a different way than the solidity does. More understandable to user story + int256 expectedPoolTotalValue = + int256(principalTokensCommitted) + + netLiquidatorPayment + + excessRepaidAmount + - int256(originalLoanPrincipalUnpaid) + + int256(interestAmount); + + + + assertEq(poolTotalEstimatedValue , uint256( expectedPoolTotalValue )); + + + } + + /* improve tests for this