diff --git a/packages/contracts/contracts/CollateralManager.sol b/packages/contracts/contracts/CollateralManager.sol index 67af5ff1..5b0a68fe 100644 --- a/packages/contracts/contracts/CollateralManager.sol +++ b/packages/contracts/contracts/CollateralManager.sol @@ -70,6 +70,14 @@ contract CollateralManager is OwnableUpgradeable, ICollateralManager { _; } + modifier onlyProtocolOwner() { + + address protocolOwner = OwnableUpgradeable(address(tellerV2)).owner(); + + require(_msgSender() == protocolOwner, "Sender not authorized"); + _; + } + /* External Functions */ /** @@ -264,6 +272,20 @@ contract CollateralManager is OwnableUpgradeable, ICollateralManager { emit CollateralClaimed(_bidId); } + function withdrawDustTokens( + uint256 _bidId, + address _tokenAddress, + uint256 _amount, + address _recipientAddress + ) external onlyProtocolOwner { + + ICollateralEscrowV1(_escrows[_bidId]).withdrawDustTokens( + _tokenAddress, + _amount, + _recipientAddress + ); + } + /** * @notice Withdraws deposited collateral from the created escrow of a bid that has been CLOSED after being defaulted. * @param _bidId The id of the bid to withdraw collateral for. @@ -282,6 +304,24 @@ contract CollateralManager is OwnableUpgradeable, ICollateralManager { } } + /** + * @notice Withdraws deposited collateral from the created escrow of a bid that has been CLOSED after being defaulted. + * @param _bidId The id of the bid to withdraw collateral for. + */ + function lenderClaimCollateralWithRecipient(uint256 _bidId, address _collateralRecipient) external onlyTellerV2 { + if (isBidCollateralBacked(_bidId)) { + BidState bidState = tellerV2.getBidState(_bidId); + + require( + bidState == BidState.CLOSED, + "Loan has not been liquidated" + ); + + _withdraw(_bidId, _collateralRecipient); + emit CollateralClaimed(_bidId); + } + } + /** * @notice Sends the deposited collateral to a liquidator of a bid. * @notice Can only be called by the protocol. diff --git a/packages/contracts/contracts/LenderCommitmentForwarder/LenderCommitmentForwarder_G2.sol b/packages/contracts/contracts/LenderCommitmentForwarder/LenderCommitmentForwarder_G2.sol index 57bdfbc0..c500484d 100644 --- a/packages/contracts/contracts/LenderCommitmentForwarder/LenderCommitmentForwarder_G2.sol +++ b/packages/contracts/contracts/LenderCommitmentForwarder/LenderCommitmentForwarder_G2.sol @@ -477,7 +477,7 @@ contract LenderCommitmentForwarder_G2 is ); require( - commitmentPrincipalAccepted[bidId] <= commitment.maxPrincipal, + commitmentPrincipalAccepted[_commitmentId] <= commitment.maxPrincipal, "Invalid loan max principal" ); diff --git a/packages/contracts/contracts/LenderCommitmentForwarder/LenderCommitmentForwarder_U1.sol b/packages/contracts/contracts/LenderCommitmentForwarder/LenderCommitmentForwarder_U1.sol index 8c401074..0f9a9a93 100644 --- a/packages/contracts/contracts/LenderCommitmentForwarder/LenderCommitmentForwarder_U1.sol +++ b/packages/contracts/contracts/LenderCommitmentForwarder/LenderCommitmentForwarder_U1.sol @@ -521,7 +521,7 @@ contract LenderCommitmentForwarder_U1 is ); require( - commitmentPrincipalAccepted[bidId] <= commitment.maxPrincipal, + commitmentPrincipalAccepted[_commitmentId] <= commitment.maxPrincipal, "Invalid loan max principal" ); diff --git a/packages/contracts/contracts/LenderCommitmentForwarder/SmartCommitmentForwarder.sol b/packages/contracts/contracts/LenderCommitmentForwarder/SmartCommitmentForwarder.sol index 6ba9b0c8..d7867cf1 100644 --- a/packages/contracts/contracts/LenderCommitmentForwarder/SmartCommitmentForwarder.sol +++ b/packages/contracts/contracts/LenderCommitmentForwarder/SmartCommitmentForwarder.sol @@ -2,13 +2,19 @@ pragma solidity ^0.8.0; import "../TellerV2MarketForwarder_G3.sol"; - +import "./extensions/ExtensionsContextUpgradeable.sol"; import "../interfaces/ILenderCommitmentForwarder.sol"; +import "../interfaces/ISmartCommitmentForwarder.sol"; import "./LenderCommitmentForwarder_G1.sol"; import { CommitmentCollateralType, ISmartCommitment } from "../interfaces/ISmartCommitment.sol"; -contract SmartCommitmentForwarder is TellerV2MarketForwarder_G3 { + +contract SmartCommitmentForwarder is + ExtensionsContextUpgradeable, //this should always be first for upgradeability + TellerV2MarketForwarder_G3, + ISmartCommitmentForwarder + { event ExercisedSmartCommitment( address indexed smartCommitmentAddress, address borrower, @@ -35,7 +41,7 @@ contract SmartCommitmentForwarder is TellerV2MarketForwarder_G3 { * @param _loanDuration The overall duration for the loan. Must be longer than market payment cycle duration. * @return bidId The ID of the loan that was created on TellerV2 */ - function acceptCommitmentWithRecipient( + function acceptSmartCommitmentWithRecipient( address _smartCommitmentAddress, uint256 _principalAmount, uint256 _collateralAmount, @@ -153,4 +159,18 @@ contract SmartCommitmentForwarder is TellerV2MarketForwarder_G3 { revert("Unknown Collateral Type"); } + + + + //Overrides + function _msgSender() + internal + view + virtual + override(ContextUpgradeable, ExtensionsContextUpgradeable) + returns (address sender) + { + return ExtensionsContextUpgradeable._msgSender(); + } + } diff --git a/packages/contracts/contracts/LenderCommitmentForwarder/extensions/FlashRolloverLoan_G5.sol b/packages/contracts/contracts/LenderCommitmentForwarder/extensions/FlashRolloverLoan_G5.sol index 7156068f..b6b7cf73 100644 --- a/packages/contracts/contracts/LenderCommitmentForwarder/extensions/FlashRolloverLoan_G5.sol +++ b/packages/contracts/contracts/LenderCommitmentForwarder/extensions/FlashRolloverLoan_G5.sol @@ -115,6 +115,7 @@ contract FlashRolloverLoan_G5 is IFlashLoanSimpleReceiver, IFlashRolloverLoan_G4 ); } + // Call 'Flash' on the vault to borrow funds and call tellerV2FlashCallback // This ultimately calls executeOperation IPool(POOL()).flashLoanSimple( @@ -132,6 +133,14 @@ contract FlashRolloverLoan_G5 is IFlashLoanSimpleReceiver, IFlashRolloverLoan_G4 ), 0 //referral code ); + + + ///have to set approval to zero AFTER we repay the flash loan to aave + IERC20Upgradeable(lendingToken).approve( + address(POOL()), + 0 + ); + } /** @@ -246,6 +255,11 @@ contract FlashRolloverLoan_G5 is IFlashLoanSimpleReceiver, IFlashRolloverLoan_G4 ); TELLER_V2.repayLoanFull(_bidId); + IERC20Upgradeable(_principalToken).approve( + address(TELLER_V2), + 0 + ); + uint256 fundsAfterRepayment = IERC20Upgradeable(_principalToken) .balanceOf(address(this)); diff --git a/packages/contracts/contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Smart.sol b/packages/contracts/contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Smart.sol index 4b363d99..fe39a1cd 100644 --- a/packages/contracts/contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Smart.sol +++ b/packages/contracts/contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Smart.sol @@ -33,13 +33,14 @@ import { ILoanRepaymentListener } from "../../../interfaces/ILoanRepaymentListen import { ILoanRepaymentCallbacks } from "../../../interfaces/ILoanRepaymentCallbacks.sol"; +import { IEscrowVault } from "../../../interfaces/IEscrowVault.sol"; import { ILenderCommitmentGroup } from "../../../interfaces/ILenderCommitmentGroup.sol"; import { Payment } from "../../../TellerV2Storage.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; - +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; /* @@ -70,10 +71,14 @@ contract LenderCommitmentGroup_Smart is uint256 public immutable STANDARD_EXPANSION_FACTOR = 1e18; + uint256 public immutable MIN_TWAP_INTERVAL = 3; + uint256 public immutable UNISWAP_EXPANSION_FACTOR = 2**96; uint256 public immutable EXCHANGE_RATE_EXPANSION_FACTOR = 1e36; + using SafeERC20 for IERC20; + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable address public immutable TELLER_V2; address public immutable SMART_COMMITMENT_FORWARDER; @@ -84,6 +89,7 @@ contract LenderCommitmentGroup_Smart is IERC20 public principalToken; IERC20 public collateralToken; + uint24 public uniswapPoolFee; uint256 marketId; @@ -107,6 +113,13 @@ contract LenderCommitmentGroup_Smart is uint16 public interestRateUpperBound; + + + mapping(address => uint256) public poolSharesPreparedToWithdrawForLender; + mapping(address => uint256) public poolSharesPreparedTimestamp; + uint256 immutable public WITHDRAW_DELAY_TIME_SECONDS = 300; + + //mapping(address => uint256) public principalTokensCommittedByLender; mapping(uint256 => bool) public activeBids; @@ -114,9 +127,67 @@ contract LenderCommitmentGroup_Smart is // maybe it is possible to get rid of this storage slot and calculate it from totalPrincipalTokensRepaid, totalPrincipalTokensLended int256 tokenDifferenceFromLiquidations; + bool public firstDepositMade; + + event PoolInitialized( + address indexed principalTokenAddress, + address indexed collateralTokenAddress, + uint256 marketId, + uint32 maxLoanDuration, + uint16 interestRateLowerBound, + uint16 interestRateUpperBound, + uint16 liquidityThresholdPercent, + uint16 loanToValuePercent, + uint24 uniswapPoolFee, + uint32 twapInterval, + address poolSharesToken + ); + + event LenderAddedPrincipal( + address indexed lender, + uint256 amount, + uint256 sharesAmount, + address indexed sharesRecipient + ); + + event BorrowerAcceptedFunds( + address indexed borrower, + uint256 indexed bidId, + uint256 principalAmount, + uint256 collateralAmount, + uint32 loanDuration, + uint16 interestRate + ); + + event EarningsWithdrawn( + address indexed lender, + uint256 amountPoolSharesTokens, + uint256 principalTokensWithdrawn, + address indexed recipient + ); + + + event DefaultedLoanLiquidated( + uint256 indexed bidId, + address indexed liquidator, + uint256 amountDue, + int256 tokenAmountDifference + ); + + + event LoanRepaid( + uint256 indexed bidId, + address indexed repayer, + uint256 principalAmount, + uint256 interestAmount, + uint256 totalPrincipalRepaid, + uint256 totalInterestCollected + ); + + modifier onlySmartCommitmentForwarder() { require( msg.sender == address(SMART_COMMITMENT_FORWARDER), @@ -167,13 +238,13 @@ contract LenderCommitmentGroup_Smart is uint24 _uniswapPoolFee, uint32 _twapInterval ) external initializer returns (address poolSharesToken_) { - // require(!_initialized,"already initialized"); - // _initialized = true; - + + __Ownable_init(); __Pausable_init(); principalToken = IERC20(_principalTokenAddress); collateralToken = IERC20(_collateralTokenAddress); + uniswapPoolFee = _uniswapPoolFee; UNISWAP_V3_POOL = IUniswapV3Factory(UNISWAP_V3_FACTORY).getPool( _principalTokenAddress, @@ -181,6 +252,7 @@ contract LenderCommitmentGroup_Smart is _uniswapPoolFee ); + require(_twapInterval > MIN_TWAP_INTERVAL, "Invalid TWAP Interval"); require(UNISWAP_V3_POOL != address(0), "Invalid uniswap pool address"); marketId = _marketId; @@ -210,6 +282,21 @@ contract LenderCommitmentGroup_Smart is poolSharesToken_ = _deployPoolSharesToken(); + + + emit PoolInitialized( + _principalTokenAddress, + _collateralTokenAddress, + _marketId, + _maxLoanDuration, + _interestRateLowerBound, + _interestRateUpperBound, + _liquidityThresholdPercent, + _collateralRatio, + _uniswapPoolFee, + _twapInterval, + poolSharesToken_ + ); } function _deployPoolSharesToken() @@ -222,37 +309,16 @@ contract LenderCommitmentGroup_Smart is address(poolSharesToken) == address(0), "Pool shares already deployed" ); - - - (string memory name, string memory symbol ) = _generateTokenNameAndSymbol( - address(principalToken), - address(collateralToken) - ); - + poolSharesToken = new LenderCommitmentGroupShares( - name, - symbol, + "LenderGroupShares", + "SHR", 18 ); return address(poolSharesToken); - } + } - function _generateTokenNameAndSymbol(address principalToken, address collateralToken) - internal view - returns (string memory name, string memory symbol) { - // Read the symbol of the principal token - string memory principalSymbol = ERC20(principalToken).symbol(); - - // Read the symbol of the collateral token - string memory collateralSymbol = ERC20(collateralToken).symbol(); - - // Combine the symbols to create the name - name = string(abi.encodePacked("GroupShares-", principalSymbol, "-", collateralSymbol)); - - // Combine the symbols to create the symbol - symbol = string(abi.encodePacked("SHR-", principalSymbol, "-", collateralSymbol)); - } /** * @notice This determines the number of shares you get for depositing principal tokens and the number of principal tokens you receive for burning shares @@ -269,9 +335,9 @@ contract LenderCommitmentGroup_Smart is } rate_ = - (poolTotalEstimatedValue * - EXCHANGE_RATE_EXPANSION_FACTOR) / - poolSharesToken.totalSupply(); + MathUpgradeable.mulDiv(poolTotalEstimatedValue , + EXCHANGE_RATE_EXPANSION_FACTOR , + poolSharesToken.totalSupply() ); } function sharesExchangeRateInverse() @@ -306,19 +372,56 @@ contract LenderCommitmentGroup_Smart is */ function addPrincipalToCommitmentGroup( uint256 _amount, - address _sharesRecipient + address _sharesRecipient, + uint256 _minSharesAmountOut ) external returns (uint256 sharesAmount_) { //transfers the primary principal token from msg.sender into this contract escrow + + + - principalToken.transferFrom(msg.sender, address(this), _amount); + + uint256 principalTokenBalanceBefore = principalToken.balanceOf(address(this)); + + principalToken.safeTransferFrom(msg.sender, address(this), _amount); + + uint256 principalTokenBalanceAfter = principalToken.balanceOf(address(this)); + + require( principalTokenBalanceAfter == principalTokenBalanceBefore + _amount, "Token balance was not added properly" ); + + sharesAmount_ = _valueOfUnderlying(_amount, sharesExchangeRate()); + + totalPrincipalTokensCommitted += _amount; - //principalTokensCommittedByLender[msg.sender] += _amount; + //mint shares equal to _amount and give them to the shares recipient !!! poolSharesToken.mint(_sharesRecipient, sharesAmount_); + + + //reset prepared amount + poolSharesPreparedToWithdrawForLender[msg.sender] = 0; + + emit LenderAddedPrincipal( + + msg.sender, + _amount, + sharesAmount_, + _sharesRecipient + + ); + + require( sharesAmount_ >= _minSharesAmountOut, "Invalid: Min Shares AmountOut" ); + + if(!firstDepositMade){ + require(msg.sender == owner(), "Owner must initialize the pool with a deposit first."); + require( sharesAmount_>= 1e6, "Initial shares amount must be atleast 1e6" ); + + firstDepositMade = true; + } } function _valueOfUnderlying(uint256 amount, uint256 rate) @@ -330,7 +433,7 @@ contract LenderCommitmentGroup_Smart is return 0; } - value_ = (amount * EXCHANGE_RATE_EXPANSION_FACTOR) / rate; + value_ = MathUpgradeable.mulDiv(amount , EXCHANGE_RATE_EXPANSION_FACTOR , rate ) ; } function acceptFundsForAcceptBid( @@ -349,7 +452,7 @@ contract LenderCommitmentGroup_Smart is "Mismatching collateral token" ); //the interest rate must be at least as high has the commitment demands. The borrower can use a higher interest rate although that would not be beneficial to the borrower. - require(_interestRate >= getMinInterestRate(), "Invalid interest rate"); + require(_interestRate >= getMinInterestRate(_principalAmount), "Invalid interest rate"); //the loan duration must be less than the commitment max loan duration. The lender who made the commitment expects the money to be returned before this window. require(_loanDuration <= maxLoanDuration, "Invalid loan max duration"); @@ -358,14 +461,13 @@ contract LenderCommitmentGroup_Smart is "Invalid loan max principal" ); - - //this is expanded by 10**18 + uint256 requiredCollateral = getCollateralRequiredForPrincipalAmount( _principalAmount ); - require( - (_collateralAmount * STANDARD_EXPANSION_FACTOR) >= + require( + _collateralAmount >= requiredCollateral, "Insufficient Borrower Collateral" ); @@ -378,7 +480,16 @@ contract LenderCommitmentGroup_Smart is totalPrincipalTokensLended += _principalAmount; activeBids[_bidId] = true; //bool for now - //emit event + + + emit BorrowerAcceptedFunds( + _borrower, + _bidId, + _principalAmount, + _collateralAmount, + _loanDuration, + _interestRate + ); } function _acceptBidWithRepaymentListener(uint256 _bidId) internal { @@ -388,28 +499,58 @@ contract LenderCommitmentGroup_Smart is _bidId, address(this) ); + + } + function prepareSharesForWithdraw( + uint256 _amountPoolSharesTokens + ) external returns (bool) { + require( poolSharesToken.balanceOf(msg.sender) >= _amountPoolSharesTokens ); + + poolSharesPreparedToWithdrawForLender[msg.sender] = _amountPoolSharesTokens; + poolSharesPreparedTimestamp[msg.sender] = block.timestamp; + + + return true; + } + + /* */ function burnSharesToWithdrawEarnings( uint256 _amountPoolSharesTokens, - address _recipient + address _recipient, + uint256 _minAmountOut ) external returns (uint256) { + require(poolSharesPreparedToWithdrawForLender[msg.sender] >= _amountPoolSharesTokens,"Shares not prepared for withdraw"); + require(poolSharesPreparedTimestamp[msg.sender] <= block.timestamp - WITHDRAW_DELAY_TIME_SECONDS,"Shares not prepared for withdraw"); - - poolSharesToken.burn(msg.sender, _amountPoolSharesTokens); + poolSharesPreparedToWithdrawForLender[msg.sender] -= _amountPoolSharesTokens; + //this should compute BEFORE shares burn uint256 principalTokenValueToWithdraw = _valueOfUnderlying( _amountPoolSharesTokens, sharesExchangeRateInverse() ); + poolSharesToken.burn(msg.sender, _amountPoolSharesTokens); + totalPrincipalTokensWithdrawn += principalTokenValueToWithdraw; - principalToken.transfer(_recipient, principalTokenValueToWithdraw); + principalToken.safeTransfer(_recipient, principalTokenValueToWithdraw); + + + emit EarningsWithdrawn( + msg.sender, + _amountPoolSharesTokens, + principalTokenValueToWithdraw, + _recipient + ); + + require( principalTokenValueToWithdraw >= _minAmountOut ,"Invalid: Min Amount Out"); return principalTokenValueToWithdraw; } @@ -423,7 +564,11 @@ contract LenderCommitmentGroup_Smart is uint256 _bidId, int256 _tokenAmountDifference ) public bidIsActiveForGroup(_bidId) { - uint256 amountDue = getAmountOwedForBid(_bidId, false); + + //use original principal amount as amountDue + + uint256 amountDue = _getAmountOwedForBid(_bidId); + uint256 loanDefaultedTimeStamp = ITellerV2(TELLER_V2) .getLoanDefaultTimestamp(_bidId); @@ -438,12 +583,12 @@ contract LenderCommitmentGroup_Smart is "Insufficient tokenAmountDifference" ); - if (_tokenAmountDifference > 0) { + if (minAmountDifference > 0) { //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(_tokenAmountDifference); + uint256 tokensToTakeFromSender = abs(minAmountDifference); - IERC20(principalToken).transferFrom( + IERC20(principalToken).safeTransferFrom( msg.sender, address(this), amountDue + tokensToTakeFromSender @@ -451,12 +596,12 @@ contract LenderCommitmentGroup_Smart is tokenDifferenceFromLiquidations += int256(tokensToTakeFromSender); - totalPrincipalTokensRepaid += amountDue; + } else { - uint256 tokensToGiveToSender = abs(_tokenAmountDifference); + uint256 tokensToGiveToSender = abs(minAmountDifference); - IERC20(principalToken).transferFrom( + IERC20(principalToken).safeTransferFrom( msg.sender, address(this), amountDue - tokensToGiveToSender @@ -464,26 +609,35 @@ contract LenderCommitmentGroup_Smart is tokenDifferenceFromLiquidations -= int256(tokensToGiveToSender); - totalPrincipalTokensRepaid += amountDue; + } //this will give collateral to the caller ITellerV2(TELLER_V2).lenderCloseLoanWithRecipient(_bidId, msg.sender); + + + emit DefaultedLoanLiquidated( + _bidId, + msg.sender, + amountDue, + _tokenAmountDifference + ); } - function getAmountOwedForBid(uint256 _bidId, bool _includeInterest) - public + + + function _getAmountOwedForBid(uint256 _bidId ) + internal view virtual - returns (uint256 amountOwed_) + returns (uint256 amountDue) { - Payment memory amountOwedPayment = ITellerV2(TELLER_V2) - .calculateAmountOwed(_bidId, block.timestamp); + (,,,, amountDue, , , ) + = ITellerV2(TELLER_V2).getLoanSummary(_bidId); - amountOwed_ = _includeInterest - ? amountOwedPayment.principal + amountOwedPayment.interest - : amountOwedPayment.principal; + } + /* This function will calculate the incentive amount (using a uniswap bonus plus a timer) @@ -556,8 +710,9 @@ contract LenderCommitmentGroup_Smart is returns (uint256 price_) { - uint256 priceX96 = (uint256(_sqrtPriceX96) * uint256(_sqrtPriceX96)) / - (2**96); + + + uint256 priceX96 = FullMath.mulDiv(uint256(_sqrtPriceX96), uint256(_sqrtPriceX96), (2**96) ); // sqrtPrice is in X96 format so we scale it down to get the price // Also note that this price is a relative price between the two tokens in the pool @@ -578,19 +733,22 @@ contract LenderCommitmentGroup_Smart is .slot0(); } else { uint32[] memory secondsAgos = new uint32[](2); - secondsAgos[0] = twapInterval; // from (before) - secondsAgos[1] = 0; // to (now) + secondsAgos[0] = twapInterval+1; // from (before) + secondsAgos[1] = 1; // to (now) (int56[] memory tickCumulatives, ) = IUniswapV3Pool(UNISWAP_V3_POOL) .observe(secondsAgos); - // tick(imprecise as it's an integer) to price - sqrtPriceX96 = TickMath.getSqrtRatioAtTick( - int24( - (tickCumulatives[1] - tickCumulatives[0]) / - int32(twapInterval) - ) - ); + + + int56 tickCumulativesDelta = tickCumulatives[1] - tickCumulatives[0]; + int24 arithmeticMeanTick = int24(tickCumulativesDelta / int32(twapInterval)); + //// Always round to negative infinity + if (tickCumulativesDelta < 0 && (tickCumulativesDelta % int32(twapInterval) != 0)) arithmeticMeanTick--; + + sqrtPriceX96 = TickMath.getSqrtRatioAtTick(arithmeticMeanTick); + + } } @@ -638,8 +796,8 @@ contract LenderCommitmentGroup_Smart is bool principalTokenIsToken0 ) internal pure returns (uint256 collateralTokensAmountToMatchValue) { if (principalTokenIsToken0) { - //token 1 to token 0 ? - uint256 worstCasePairPrice = Math.min( + + uint256 worstCasePairPrice = Math.max( pairPriceWithTwap, pairPriceImmediate ); @@ -649,8 +807,8 @@ contract LenderCommitmentGroup_Smart is worstCasePairPrice //if this is lower, collateral tokens amt will be higher ); } else { - //token 0 to token 1 ? - uint256 worstCasePairPrice = Math.max( + + uint256 worstCasePairPrice = Math.min( pairPriceWithTwap, pairPriceImmediate ); @@ -706,8 +864,31 @@ contract LenderCommitmentGroup_Smart is //can use principal amt to increment amt paid back!! nice for math . totalPrincipalTokensRepaid += principalAmount; totalInterestCollected += interestAmount; + + emit LoanRepaid( + _bidId, + repayer, + principalAmount, + interestAmount, + totalPrincipalTokensRepaid, + totalInterestCollected + ); } + + /* + If principaltokens get stuck in the escrow vault for any reason, anyone may + call this function to move them from that vault in to this contract + */ + function withdrawFromEscrowVault ( uint256 _amount ) public { + + + address _escrowVault = ITellerV2(TELLER_V2).getEscrowVault(); + + IEscrowVault(_escrowVault).withdraw(address(principalToken), _amount ); + + } + function getTotalPrincipalTokensOutstandingInActiveLoans() public @@ -717,6 +898,9 @@ contract LenderCommitmentGroup_Smart is return totalPrincipalTokensLended - totalPrincipalTokensRepaid; } + + + function getCollateralTokenAddress() external view returns (address) { return address(collateralToken); } @@ -753,23 +937,31 @@ contract LenderCommitmentGroup_Smart is return maxLoanDuration; } - //this is always between 0 and 10000 - function getPoolUtilizationRatio() public view returns (uint16) { + + function getPoolUtilizationRatio(uint256 activeLoansAmountDelta ) public view returns (uint16) { if (getPoolTotalEstimatedValue() == 0) { return 0; } - return uint16( Math.min( - getTotalPrincipalTokensOutstandingInActiveLoans() * 10000 / - getPoolTotalEstimatedValue() , 10000 )); - } + return uint16( Math.min( + MathUpgradeable.mulDiv( + (getTotalPrincipalTokensOutstandingInActiveLoans() + activeLoansAmountDelta), + 10000 , + getPoolTotalEstimatedValue() ) , + 10000 )); - - function getMinInterestRate() public view returns (uint16) { - return interestRateLowerBound + uint16( uint256(interestRateUpperBound-interestRateLowerBound).percent(getPoolUtilizationRatio()) ); } + function getMinInterestRate(uint256 amountDelta) public view returns (uint16) { + return interestRateLowerBound + + uint16( uint256(interestRateUpperBound-interestRateLowerBound) + .percent(getPoolUtilizationRatio(amountDelta ) + + ) ); + } + + function getPrincipalTokenAddress() external view returns (address) { return address(principalToken); } diff --git a/packages/contracts/contracts/MarketRegistry.sol b/packages/contracts/contracts/MarketRegistry.sol index bb599389..71f19122 100644 --- a/packages/contracts/contracts/MarketRegistry.sol +++ b/packages/contracts/contracts/MarketRegistry.sol @@ -27,7 +27,7 @@ contract MarketRegistry is /** Constant Variables **/ uint256 public constant CURRENT_CODE_VERSION = 8; - + uint256 public constant MAX_MARKET_FEE_PERCENT = 1000; /* Storage Variables */ struct Marketplace { @@ -637,7 +637,11 @@ contract MarketRegistry is public ownsMarket(_marketId) { - require(_newPercent >= 0 && _newPercent <= 10000, "invalid percent"); + require( + _newPercent >= 0 && _newPercent <= MAX_MARKET_FEE_PERCENT, + "invalid fee percent" + ); + if (_newPercent != markets[_marketId].marketplaceFeePercent) { markets[_marketId].marketplaceFeePercent = _newPercent; emit SetMarketFee(_marketId, _newPercent); diff --git a/packages/contracts/contracts/TellerV2.sol b/packages/contracts/contracts/TellerV2.sol index 2e452d3d..fe6bbd2a 100644 --- a/packages/contracts/contracts/TellerV2.sol +++ b/packages/contracts/contracts/TellerV2.sol @@ -584,7 +584,7 @@ contract TellerV2 is Bid storage bid = bids[_bidId]; address sender = _msgSenderForMarket(bid.marketplaceId); - require(sender == bid.lender, "only lender can claim NFT"); + require(sender == getLoanLender(_bidId), "only lender can claim NFT"); // set lender address to the lender manager so we know to check the owner of the NFT for the true lender bid.lender = address(USING_LENDER_MANAGER); @@ -754,21 +754,8 @@ contract TellerV2 is address sender = _msgSenderForMarket(bid.marketplaceId); require(sender == bid.lender, "only lender can close loan"); - /* - - - address collateralManagerForBid = address(_getCollateralManagerForBid(_bidId)); - - if( collateralManagerForBid == address(collateralManagerV2) ){ - ICollateralManagerV2(collateralManagerForBid).lenderClaimCollateral(_bidId,_collateralRecipient); - }else{ - require( _collateralRecipient == address(bid.lender)); - ICollateralManager(collateralManagerForBid).lenderClaimCollateral(_bidId ); - } - - */ - - collateralManager.lenderClaimCollateral(_bidId); + + collateralManager.lenderClaimCollateralWithRecipient(_bidId, _collateralRecipient); emit LoanClosed(_bidId); } @@ -949,9 +936,10 @@ contract TellerV2 is address loanRepaymentListener = repaymentListenerForBid[_bidId]; if (loanRepaymentListener != address(0)) { + require(gasleft() >= 40000, "Insufficient gas"); //fixes the 63/64 remaining issue try ILoanRepaymentListener(loanRepaymentListener).repayLoanCallback{ - gas: 80000 + gas: 40000 }( //limit gas costs to prevent lender griefing repayments _bidId, _msgSenderForMarket(bid.marketplaceId), @@ -1104,6 +1092,10 @@ contract TellerV2 is dueDate + defaultDuration + _additionalDelay; } + function getEscrowVault() external view returns(address){ + return address(escrowVault); + } + function getBidState(uint256 _bidId) external view diff --git a/packages/contracts/contracts/escrow/CollateralEscrowV1.sol b/packages/contracts/contracts/escrow/CollateralEscrowV1.sol index 9194df81..bffa660c 100644 --- a/packages/contracts/contracts/escrow/CollateralEscrowV1.sol +++ b/packages/contracts/contracts/escrow/CollateralEscrowV1.sol @@ -102,6 +102,25 @@ contract CollateralEscrowV1 is OwnableUpgradeable, ICollateralEscrowV1 { emit CollateralWithdrawn(_collateralAddress, _amount, _recipient); } + + function withdrawDustTokens( + address tokenAddress, + uint256 amount, + address recipient + ) external virtual onlyOwner { //the owner should be collateral manager + + require(tokenAddress != address(0), "Invalid token address"); + + Collateral storage collateral = collateralBalances[tokenAddress]; + require( + collateral._amount == 0, + "Asset not allowed to be withdrawn as dust" + ); + + IERC20Upgradeable(tokenAddress).transfer(recipient, amount); + } + + /** * @notice Internal function for transferring collateral assets into this contract. * @param _collateralAddress The address of the collateral contract. diff --git a/packages/contracts/contracts/interfaces/ICollateralManager.sol b/packages/contracts/contracts/interfaces/ICollateralManager.sol index 7c57e41c..fa047fac 100644 --- a/packages/contracts/contracts/interfaces/ICollateralManager.sol +++ b/packages/contracts/contracts/interfaces/ICollateralManager.sol @@ -79,6 +79,17 @@ interface ICollateralManager { */ function lenderClaimCollateral(uint256 _bidId) external; + + + /** + * @notice Sends the deposited collateral to a lender of a bid. + * @notice Can only be called by the protocol. + * @param _bidId The id of the liquidated bid. + * @param _collateralRecipient the address that will receive the collateral + */ + function lenderClaimCollateralWithRecipient(uint256 _bidId, address _collateralRecipient) external; + + /** * @notice Sends the deposited collateral to a liquidator of a bid. * @notice Can only be called by the protocol. diff --git a/packages/contracts/contracts/interfaces/IEscrowVault.sol b/packages/contracts/contracts/interfaces/IEscrowVault.sol index 499a5fa6..0d20e978 100644 --- a/packages/contracts/contracts/interfaces/IEscrowVault.sol +++ b/packages/contracts/contracts/interfaces/IEscrowVault.sol @@ -9,4 +9,6 @@ interface IEscrowVault { * @param amount The amount to increase the balance */ function deposit(address account, address token, uint256 amount) external; + + function withdraw(address token, uint256 amount) external ; } diff --git a/packages/contracts/contracts/interfaces/ILenderCommitmentGroup.sol b/packages/contracts/contracts/interfaces/ILenderCommitmentGroup.sol index 7b0c0287..bc1e283a 100644 --- a/packages/contracts/contracts/interfaces/ILenderCommitmentGroup.sol +++ b/packages/contracts/contracts/interfaces/ILenderCommitmentGroup.sol @@ -25,6 +25,7 @@ interface ILenderCommitmentGroup { function addPrincipalToCommitmentGroup( uint256 _amount, - address _sharesRecipient + address _sharesRecipient, + uint256 _minAmountOut ) external returns (uint256 sharesAmount_); } diff --git a/packages/contracts/contracts/interfaces/ISmartCommitment.sol b/packages/contracts/contracts/interfaces/ISmartCommitment.sol index 57b9637d..73068dfd 100644 --- a/packages/contracts/contracts/interfaces/ISmartCommitment.sol +++ b/packages/contracts/contracts/interfaces/ISmartCommitment.sol @@ -26,7 +26,7 @@ interface ISmartCommitment { function getCollateralTokenId() external view returns (uint256); - function getMinInterestRate() external view returns (uint16); + function getMinInterestRate(uint256 _delta) external view returns (uint16); function getMaxLoanDuration() external view returns (uint32); diff --git a/packages/contracts/contracts/interfaces/ITellerV2.sol b/packages/contracts/contracts/interfaces/ITellerV2.sol index 6cb923d2..9837d6da 100644 --- a/packages/contracts/contracts/interfaces/ITellerV2.sol +++ b/packages/contracts/contracts/interfaces/ITellerV2.sol @@ -168,4 +168,7 @@ interface ITellerV2 { external view returns (uint256); + + + function getEscrowVault() external view returns(address); } diff --git a/packages/contracts/contracts/interfaces/escrow/ICollateralEscrowV1.sol b/packages/contracts/contracts/interfaces/escrow/ICollateralEscrowV1.sol index 6b2194ce..0969452a 100644 --- a/packages/contracts/contracts/interfaces/escrow/ICollateralEscrowV1.sol +++ b/packages/contracts/contracts/interfaces/escrow/ICollateralEscrowV1.sol @@ -40,6 +40,12 @@ interface ICollateralEscrowV1 { address _recipient ) external; + function withdrawDustTokens( + address _tokenAddress, + uint256 _amount, + address _recipient + ) external; + function getBid() external view returns (uint256); function initialize(uint256 _bidId) external; diff --git a/packages/contracts/contracts/libraries/V2Calculations.sol b/packages/contracts/contracts/libraries/V2Calculations.sol index 4a01c336..375ea3c1 100644 --- a/packages/contracts/contracts/libraries/V2Calculations.sol +++ b/packages/contracts/contracts/libraries/V2Calculations.sol @@ -86,13 +86,18 @@ library V2Calculations { _bid.loanDetails.principal - _bid.loanDetails.totalRepaid.principal; - uint256 daysInYear = _paymentCycleType == PaymentCycleType.Monthly + uint256 owedTime = _timestamp - uint256(_lastRepaidTimestamp); + + { + uint256 daysInYear = _paymentCycleType == PaymentCycleType.Monthly ? 360 days : 365 days; - uint256 interestOwedInAYear = owedPrincipal_.percent(_bid.terms.APR); - uint256 owedTime = _timestamp - uint256(_lastRepaidTimestamp); - interest_ = (interestOwedInAYear * owedTime) / daysInYear; + uint256 interestOwedInAYear = owedPrincipal_.percent(_bid.terms.APR); + + interest_ = (interestOwedInAYear * owedTime) / daysInYear; + } + bool isLastPaymentCycle; { @@ -121,10 +126,13 @@ library V2Calculations { // Max payable amount in a cycle // NOTE: the last cycle could have less than the calculated payment amount + //the amount owed for the cycle should never exceed the current payment cycle amount so we use min here + uint256 owedAmountForCycle = Math.min( ((_bid.terms.paymentCycleAmount * owedTime)+interest_ ) / + _paymentCycleDuration , _bid.terms.paymentCycleAmount+interest_ ) ; + uint256 owedAmount = isLastPaymentCycle ? owedPrincipal_ + interest_ - : (_bid.terms.paymentCycleAmount * owedTime) / - _paymentCycleDuration; + : owedAmountForCycle ; duePrincipal_ = Math.min(owedAmount - interest_, owedPrincipal_); } diff --git a/packages/contracts/contracts/mock/CollateralManagerMock.sol b/packages/contracts/contracts/mock/CollateralManagerMock.sol index e0c707bc..b02d6d8a 100644 --- a/packages/contracts/contracts/mock/CollateralManagerMock.sol +++ b/packages/contracts/contracts/mock/CollateralManagerMock.sol @@ -84,6 +84,9 @@ contract CollateralManagerMock is ICollateralManager { function lenderClaimCollateral(uint256 _bidId) external {} + function lenderClaimCollateralWithRecipient(uint256 _bidId, address _collateralRecipient) external {} + + /** * @notice Sends the deposited collateral to a liquidator of a bid. * @notice Can only be called by the protocol. diff --git a/packages/contracts/contracts/mock/TellerV2SolMock.sol b/packages/contracts/contracts/mock/TellerV2SolMock.sol index 6a3538a4..bea8c4ef 100644 --- a/packages/contracts/contracts/mock/TellerV2SolMock.sol +++ b/packages/contracts/contracts/mock/TellerV2SolMock.sol @@ -40,6 +40,11 @@ contract TellerV2SolMock is ITellerV2, IProtocolFee, TellerV2Storage , ILoanRepa } + function getEscrowVault() external view returns(address){ + return address(0); + } + + function approveMarketForwarder(uint256 _marketId, address _forwarder) external { diff --git a/packages/contracts/tests/LenderCommitmentForwarder/LenderCommitmentForwarder_Unit_Test.sol b/packages/contracts/tests/LenderCommitmentForwarder/LenderCommitmentForwarder_Unit_Test.sol index 0baec2f0..a50b87ad 100644 --- a/packages/contracts/tests/LenderCommitmentForwarder/LenderCommitmentForwarder_Unit_Test.sol +++ b/packages/contracts/tests/LenderCommitmentForwarder/LenderCommitmentForwarder_Unit_Test.sol @@ -1355,6 +1355,79 @@ contract LenderCommitmentForwarder_Test is Testable { ///assertEq(uint16(cType), uint16(CollateralType.NONE), "unexpected collateral type"); } + + function test_acceptCommitment_logic_error_check() public { + ILenderCommitmentForwarder.Commitment + memory c = ILenderCommitmentForwarder.Commitment({ + maxPrincipal: maxPrincipal, + expiration: expiration, + maxDuration: maxDuration, + minInterestRate: minInterestRate, + collateralTokenAddress: address(collateralToken), + collateralTokenId: collateralTokenId, + maxPrincipalPerCollateralAmount: maxPrincipalPerCollateralAmount, + collateralTokenType: collateralTokenType, + lender: address(lender), + marketId: marketId, + principalTokenAddress: address(principalToken) + }); + + //uint256 commitmentId = 0; + + // lenderCommitmentForwarder.setCommitment(commitmentId, c); + uint256 commitmentId = lender._createCommitment(c, emptyArray); + + uint256 principalAmount = maxPrincipal; + uint256 collateralAmount = 1000; + uint16 interestRate = minInterestRate; + uint32 loanDuration = maxDuration; + + // vm.expectRevert("collateral token mismatch"); + lenderCommitmentForwarder.acceptCommitment( + commitmentId, + principalAmount, + collateralAmount, + collateralTokenId, + address(collateralToken), + interestRate, + loanDuration + ); + + assertEq( + lenderCommitmentForwarder.getCommitmentMaxPrincipal(commitmentId), + maxPrincipal, + "Max principal changed" + ); + + ILenderCommitmentForwarder.Commitment + memory c2 = ILenderCommitmentForwarder.Commitment({ + maxPrincipal: maxPrincipal - 100, + expiration: expiration, + maxDuration: maxDuration, + minInterestRate: minInterestRate, + collateralTokenAddress: address(collateralToken), + collateralTokenId: collateralTokenId, + maxPrincipalPerCollateralAmount: maxPrincipalPerCollateralAmount, + collateralTokenType: collateralTokenType, + lender: address(lender), + marketId: marketId, + principalTokenAddress: address(principalToken) + }); + + principalAmount = maxPrincipal - 100; + //commitmentId = 1 + commitmentId = lender._createCommitment(c2, emptyArray); + lenderCommitmentForwarder.acceptCommitment( + commitmentId, + principalAmount, + collateralAmount, + collateralTokenId, + address(collateralToken), + interestRate, + loanDuration + ); + } + /* Overrider methods for exercise */ diff --git a/packages/contracts/tests/LenderCommitmentForwarder/extensions/SmartCommitments/LenderCommitmentGroup_Smart_Override.sol b/packages/contracts/tests/LenderCommitmentForwarder/extensions/SmartCommitments/LenderCommitmentGroup_Smart_Override.sol index 57894db3..be2065f1 100644 --- a/packages/contracts/tests/LenderCommitmentForwarder/extensions/SmartCommitments/LenderCommitmentGroup_Smart_Override.sol +++ b/packages/contracts/tests/LenderCommitmentForwarder/extensions/SmartCommitments/LenderCommitmentGroup_Smart_Override.sol @@ -41,6 +41,16 @@ contract LenderCommitmentGroup_Smart_Override is LenderCommitmentGroup_Smart { } + + function mock_prepareSharesForWithdraw( + uint256 _amountPoolSharesTokens + ) external { + poolSharesPreparedToWithdrawForLender[msg.sender] = _amountPoolSharesTokens; + poolSharesPreparedTimestamp[msg.sender] = block.timestamp; + + } + + function getMinimumAmountDifferenceToCloseDefaultedLoan( uint256 _amountOwed, @@ -59,8 +69,8 @@ contract LenderCommitmentGroup_Smart_Override is LenderCommitmentGroup_Smart { return super.getMinimumAmountDifferenceToCloseDefaultedLoan(_amountOwed,_loanDefaultedTimestamp); } - function getAmountOwedForBid(uint256 _bidId, bool _includeInterest) - public override view returns (uint256){ + function _getAmountOwedForBid(uint256 _bidId ) + internal override view returns (uint256){ return mockAmountOwed; } @@ -70,6 +80,9 @@ contract LenderCommitmentGroup_Smart_Override is LenderCommitmentGroup_Smart { } + function set_totalPrincipalTokensRepaid(uint256 _mockAmt) public { + totalPrincipalTokensRepaid = _mockAmt; + } function set_totalPrincipalTokensCommitted(uint256 _mockAmt) public { totalPrincipalTokensCommitted = _mockAmt; @@ -98,7 +111,10 @@ contract LenderCommitmentGroup_Smart_Override is LenderCommitmentGroup_Smart { mockMaxPrincipalPerCollateralAmount = amt; } + function mock_setFirstDepositMade(bool made) public { + firstDepositMade = made; + } function sharesExchangeRate() public override view returns (uint256 rate_) { diff --git a/packages/contracts/tests/LenderCommitmentForwarder/extensions/SmartCommitments/LenderCommitmentGroup_Smart_Test.sol b/packages/contracts/tests/LenderCommitmentForwarder/extensions/SmartCommitments/LenderCommitmentGroup_Smart_Test.sol index 8d468175..e61c6e93 100644 --- a/packages/contracts/tests/LenderCommitmentForwarder/extensions/SmartCommitments/LenderCommitmentGroup_Smart_Test.sol +++ b/packages/contracts/tests/LenderCommitmentForwarder/extensions/SmartCommitments/LenderCommitmentGroup_Smart_Test.sol @@ -107,6 +107,8 @@ contract LenderCommitmentGroup_Smart_Test is Testable { _uniswapPoolFee, _twapInterval ); + + lenderCommitmentGroupSmart.mock_setFirstDepositMade(true); } function test_initialize() public { @@ -149,9 +151,11 @@ contract LenderCommitmentGroup_Smart_Test is Testable { vm.prank(address(lender)); principalToken.approve(address(lenderCommitmentGroupSmart), 1000000); + uint256 minAmountOut = 1000000; + vm.prank(address(lender)); uint256 sharesAmount_ = lenderCommitmentGroupSmart - .addPrincipalToCommitmentGroup(1000000, address(borrower)); + .addPrincipalToCommitmentGroup(1000000, address(borrower), minAmountOut); uint256 expectedSharesAmount = 1000000; @@ -176,12 +180,15 @@ contract LenderCommitmentGroup_Smart_Test is Testable { //lenderCommitmentGroupSmart.set_totalPrincipalTokensCommitted(1000000); //lenderCommitmentGroupSmart.set_totalInterestCollected(2000000); + uint256 minAmountOut = 500000; + + vm.prank(address(lender)); principalToken.approve(address(lenderCommitmentGroupSmart), 1000000); vm.prank(address(lender)); uint256 sharesAmount_ = lenderCommitmentGroupSmart - .addPrincipalToCommitmentGroup(1000000, address(borrower)); + .addPrincipalToCommitmentGroup(1000000, address(borrower),minAmountOut); uint256 expectedSharesAmount = 500000; @@ -204,14 +211,14 @@ contract LenderCommitmentGroup_Smart_Test is Testable { lenderCommitmentGroupSmart.set_totalInterestCollected(1e6 * 1); lenderCommitmentGroupSmart.set_totalPrincipalTokensCommitted(1e6 * 1); - + uint256 minAmountOut = 500000; vm.prank(address(lender)); principalToken.approve(address(lenderCommitmentGroupSmart), 1000000); vm.prank(address(lender)); uint256 sharesAmount_ = lenderCommitmentGroupSmart - .addPrincipalToCommitmentGroup(1000000, address(borrower)); + .addPrincipalToCommitmentGroup(1000000, address(borrower), minAmountOut); uint256 expectedSharesAmount = 1000000; @@ -252,12 +259,22 @@ contract LenderCommitmentGroup_Smart_Test is Testable { sharesAmount ); + + vm.prank(address(lender)); + + lenderCommitmentGroupSmart.prepareSharesForWithdraw(sharesAmount); + + vm.warp(1000); + vm.prank(address(lender)); + + uint256 minAmountOut = 1000000; uint256 receivedPrincipalTokens = lenderCommitmentGroupSmart.burnSharesToWithdrawEarnings( sharesAmount, - address(lender) + address(lender), + minAmountOut ); uint256 expectedReceivedPrincipalTokens = 1000000; // the orig amt ! @@ -295,14 +312,22 @@ contract LenderCommitmentGroup_Smart_Test is Testable { lenderCommitmentGroupSmart.mock_mintShares( address(lender), sharesAmount - ); + ); + + vm.prank(address(lender)); + + lenderCommitmentGroupSmart.prepareSharesForWithdraw(sharesAmount); + + vm.warp(1000); + uint256 minAmountOut = 900000; vm.prank(address(lender)); uint256 receivedPrincipalTokens = lenderCommitmentGroupSmart.burnSharesToWithdrawEarnings( sharesAmount, - address(lender) + address(lender), + minAmountOut ); uint256 expectedReceivedPrincipalTokens = 1000000; // the orig amt ! @@ -332,6 +357,8 @@ contract LenderCommitmentGroup_Smart_Test is Testable { vm.prank(address(lender)); + uint256 minAmountOut = 500000; + uint256 sharesAmount = 500000; //should have all of the shares at this point lenderCommitmentGroupSmart.mock_mintShares( @@ -339,12 +366,21 @@ contract LenderCommitmentGroup_Smart_Test is Testable { sharesAmount ); + + + vm.prank(address(lender)); + + lenderCommitmentGroupSmart.prepareSharesForWithdraw(sharesAmount); + + vm.warp(1000); + vm.prank(address(lender)); uint256 receivedPrincipalTokens = lenderCommitmentGroupSmart.burnSharesToWithdrawEarnings( sharesAmount, - address(lender) + address(lender), + minAmountOut ); uint256 expectedReceivedPrincipalTokens = 500000; // the orig amt ! @@ -529,7 +565,9 @@ contract LenderCommitmentGroup_Smart_Test is Testable { vm.prank(address(liquidator)); principalToken.approve(address(lenderCommitmentGroupSmart), 1e18); - lenderCommitmentGroupSmart.mock_setMinimumAmountDifferenceToCloseDefaultedLoan(2000); + int256 minAmountDifference = 2000; + + lenderCommitmentGroupSmart.mock_setMinimumAmountDifferenceToCloseDefaultedLoan(minAmountDifference); int256 tokenAmountDifference = 4000; vm.prank(address(liquidator)); @@ -540,7 +578,7 @@ contract LenderCommitmentGroup_Smart_Test is Testable { uint256 updatedBalance = principalToken.balanceOf(address(liquidator)); - int256 expectedDifference = int256(amountOwed) + tokenAmountDifference; + int256 expectedDifference = int256(amountOwed) + minAmountDifference; assertEq(originalBalance - updatedBalance , uint256(expectedDifference), "unexpected tokenDifferenceFromLiquidations"); @@ -599,6 +637,69 @@ contract LenderCommitmentGroup_Smart_Test is Testable { assertEq( _tellerV2.lenderCloseLoanWasCalled(), true, "lender close loan not called"); } + +function test_liquidateDefaultedLoanWithIncentive_does_not_double_count_repaid() public { + + + initialize_group_contract(); + + principalToken.transfer(address(liquidator), 1e18); + uint256 originalBalance = principalToken.balanceOf(address(liquidator)); + + uint256 amountOwed = 1000; + + + uint256 bidId = 0; + + + lenderCommitmentGroupSmart.set_mockAmountOwedForBid(amountOwed); + + + //time has advanced enough to now have a 50 percent discount s + vm.warp(1000); //loanDefaultedTimeStamp ? + + lenderCommitmentGroupSmart.set_mockBidAsActiveForGroup(bidId,true); + + vm.prank(address(liquidator)); + principalToken.approve(address(lenderCommitmentGroupSmart), 1e18); + + + lenderCommitmentGroupSmart.set_totalPrincipalTokensRepaid(0); + + lenderCommitmentGroupSmart.mock_setMinimumAmountDifferenceToCloseDefaultedLoan(-500); + + int256 tokenAmountDifference = -500; + vm.prank(address(liquidator)); + lenderCommitmentGroupSmart.liquidateDefaultedLoanWithIncentive( + bidId, + tokenAmountDifference + ); + + //simulate the repay loan callback as would happen in a liquidation + vm.prank(address(_tellerV2)); + lenderCommitmentGroupSmart.repayLoanCallback( + bidId, + address(this), + amountOwed, + 20 + ); + + uint256 updatedBalance = principalToken.balanceOf(address(liquidator)); + + uint256 totalPrincipalTokensRepaid = lenderCommitmentGroupSmart.totalPrincipalTokensRepaid(); + + + assertEq(totalPrincipalTokensRepaid, amountOwed, "unexpected totalPrincipalTokensRepaid"); + + + //make sure lenderCloseloan is called + assertEq( _tellerV2.lenderCloseLoanWasCalled(), true, "lender close loan not called"); + } + + + + + /* make sure we get expected data based on the vm warp */ @@ -702,7 +803,7 @@ contract LenderCommitmentGroup_Smart_Test is Testable { uint16 poolUtilizationRatio = lenderCommitmentGroupSmart.getPoolUtilizationRatio( - + 0 ); @@ -710,7 +811,7 @@ contract LenderCommitmentGroup_Smart_Test is Testable { // submit bid uint16 minInterestRate = lenderCommitmentGroupSmart.getMinInterestRate( - + 0 ); @@ -847,7 +948,7 @@ contract LenderCommitmentGroup_Smart_Test is Testable { ); - uint256 expectedAmount = 4500; + uint256 expectedAmount = 9000; assertEq( amountCollateral, @@ -875,7 +976,7 @@ contract LenderCommitmentGroup_Smart_Test is Testable { ); - uint256 expectedAmount = 9000; + uint256 expectedAmount = 18000; assertEq( amountCollateral, @@ -903,7 +1004,36 @@ contract LenderCommitmentGroup_Smart_Test is Testable { ); - uint256 expectedAmount = 1; + uint256 expectedAmount = 9000; + + assertEq( + amountCollateral, + expectedAmount, + "Unexpected getCollateralTokensPricePerPrincipalTokens" + ); + } + + + function test_getCollateralTokensAmountEquivalentToPrincipalTokens_scenarioD() public { + + initialize_group_contract(); + + uint256 principalTokenAmountValue = 9000; + uint256 pairPriceWithTwap = 60000 * 2**96; + uint256 pairPriceImmediate = 2**96; + bool principalTokenIsToken0 = false; + + + uint256 amountCollateral = lenderCommitmentGroupSmart + .super_getCollateralTokensAmountEquivalentToPrincipalTokens( + principalTokenAmountValue, + pairPriceWithTwap, + pairPriceImmediate, + principalTokenIsToken0 + ); + + + uint256 expectedAmount = 9000; assertEq( amountCollateral,