diff --git a/packages/contracts/contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Smart.sol b/packages/contracts/contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Smart.sol index 5baef9b9..9667d0f4 100644 --- a/packages/contracts/contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Smart.sol +++ b/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"; @@ -434,8 +435,14 @@ 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( 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) @@ -661,7 +668,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 +684,11 @@ contract LenderCommitmentGroup_Smart is ); int256 minAmountDifference = getMinimumAmountDifferenceToCloseDefaultedLoan( - amountDue, + loanTotalPrincipalAmount, loanDefaultedOrUnpausedAtTimeStamp ); + require( _tokenAmountDifference >= minAmountDifference, "Insufficient tokenAmountDifference" @@ -687,7 +699,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 +712,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 +736,43 @@ contract LenderCommitmentGroup_Smart is uint256 tokensToGiveToSender = abs(minAmountDifference); - - IERC20(principalToken).safeTransferFrom( - msg.sender, - address(this), - amountDue - tokensToGiveToSender - ); + + + //dont stipend/refund more than principalDue base + if (tokensToGiveToSender > principalDue) { + tokensToGiveToSender = principalDue; + } + + uint256 netAmountDue = principalDue - tokensToGiveToSender ; + - totalPrincipalTokensRepaid += amountDue; + 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,11 +780,12 @@ contract LenderCommitmentGroup_Smart is emit DefaultedLoanLiquidated( _bidId, msg.sender, - amountDue, + principalDue, _tokenAmountDifference ); } + function getLastUnpausedAt() public view @@ -766,18 +806,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) ; } @@ -966,7 +1017,11 @@ contract LenderCommitmentGroup_Smart is public view returns (uint256) - { + { + if (totalPrincipalTokensRepaid > totalPrincipalTokensLended) { + return 0; + } + return totalPrincipalTokensLended - totalPrincipalTokensRepaid; } diff --git a/packages/contracts/contracts/mock/TellerV2SolMock.sol b/packages/contracts/contracts/mock/TellerV2SolMock.sol index 8dcce0b9..3bcccc01 100644 --- a/packages/contracts/contracts/mock/TellerV2SolMock.sol +++ b/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/packages/contracts/tests/SmartCommitmentForwarder/LenderCommitmentGroup_Smart_Liquidations_Tests.sol b/packages/contracts/tests/SmartCommitmentForwarder/LenderCommitmentGroup_Smart_Liquidations_Tests.sol index 550f9dfe..d1f09aa3 100644 --- a/packages/contracts/tests/SmartCommitmentForwarder/LenderCommitmentGroup_Smart_Liquidations_Tests.sol +++ b/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/packages/contracts/tests/SmartCommitmentForwarder/LenderCommitmentGroup_Smart_Override.sol b/packages/contracts/tests/SmartCommitmentForwarder/LenderCommitmentGroup_Smart_Override.sol index 56355c7a..12903634 100644 --- a/packages/contracts/tests/SmartCommitmentForwarder/LenderCommitmentGroup_Smart_Override.sol +++ b/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,16 +74,28 @@ contract LenderCommitmentGroup_Smart_Override is LenderCommitmentGroup_Smart { } function _getAmountOwedForBid(uint256 _bidId ) - internal override view returns (uint256){ - return mockAmountOwed; + internal override view returns (uint256, uint256){ + return (mockAmountOwedPrincipal,mockAmountOwedInterest ); } - function set_mockAmountOwedForBid(uint256 _amt) public { - mockAmountOwed = _amt; + function set_mockAmountOwedForBid(uint256 _principal,uint256 _interest) public { + mockAmountOwedPrincipal = _principal; + mockAmountOwedInterest = _interest; } + + function _getLoanTotalPrincipalAmount(uint256 _bidId ) + internal override view returns (uint256){ + return mockLoanTotalPrincipalAmount; + + } + function set_mockLoanTotalPrincipalAmount(uint256 _principal) public { + mockLoanTotalPrincipalAmount = _principal; + + } + function set_totalPrincipalTokensRepaid(uint256 _mockAmt) public { totalPrincipalTokensRepaid = _mockAmt; } diff --git a/packages/contracts/tests/SmartCommitmentForwarder/LenderCommitmentGroup_Smart_Test.sol b/packages/contracts/tests/SmartCommitmentForwarder/LenderCommitmentGroup_Smart_Test.sol index 0e4812df..812fe0d6 100644 --- a/packages/contracts/tests/SmartCommitmentForwarder/LenderCommitmentGroup_Smart_Test.sol +++ b/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,911 @@ 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); + + + 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); + + + 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); + + + 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); + + + + // 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); + + + + // 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); + + + + // 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 )); + + + } + + + /* improve tests for this