diff --git a/src/Clearinghouse.sol b/src/Clearinghouse.sol index 6931054..44e7589 100644 --- a/src/Clearinghouse.sol +++ b/src/Clearinghouse.sol @@ -186,14 +186,15 @@ contract Clearinghouse is Policy, RolesConsumer, CoolerCallback { if (loan.lender != address(this)) revert NotLender(); uint256 interestNew; - if (loan.interestDue == 0) { + uint256 interestBase = interestForLoan(loan.principal, loan.request.duration); + if (loan.interestDue != interestBase) { // If interest has manually been repaid, user only pays for the subsequent extensions. - interestNew = interestForLoan(loan.principal, loan.request.duration * (times_ - 1)); + interestNew = interestBase * (times_ - 1) + loan.interestDue; // Receivables need to be updated. - interestReceivables += interestForLoan(loan.principal, loan.request.duration); + interestReceivables += interestBase - loan.interestDue; } else { // Otherwise, user pays for all the extensions. - interestNew = interestForLoan(loan.principal, loan.request.duration * times_); + interestNew = interestBase * times_; } // Transfer in extension interest from the caller. dai.transferFrom(msg.sender, loan.recipient, interestNew); @@ -267,10 +268,8 @@ contract Clearinghouse is Policy, RolesConsumer, CoolerCallback { // Reward keeper. gohm.transfer(msg.sender, keeperRewards); - - // Unstake and burn the collateral of the defaulted loans. - gohm.approve(address(staking), totalCollateral - keeperRewards); - MINTR.burnOhm(address(this), staking.unstake(address(this), totalCollateral - keeperRewards, false, false)); + // Burn the outstanding collateral of defaulted loans. + burn(); } // --- CALLBACKS ----------------------------------------------------- @@ -315,6 +314,10 @@ contract Clearinghouse is Policy, RolesConsumer, CoolerCallback { if (fundTime > block.timestamp) return false; fundTime += FUND_CADENCE; + // Sweep DAI into DSR if necessary. + uint256 idle = dai.balanceOf(address(this)); + if (idle != 0) _sweepIntoDSR(idle); + uint256 daiBalance = sdai.maxWithdraw(address(this)); uint256 outstandingDebt = TRSRY.reserveDebt(dai, address(this)); // Rebalance funds on hand with treasury's reserves. @@ -334,10 +337,6 @@ contract Clearinghouse is Policy, RolesConsumer, CoolerCallback { TRSRY.increaseWithdrawApproval(address(this), sdai, sdaiAmount); TRSRY.withdrawReserves(address(this), sdai, sdaiAmount); - // Sweep DAI into DSR if necessary. - uint256 idle = dai.balanceOf(address(this)); - if (idle != 0) _sweepIntoDSR(idle); - // Log the event. emit Rebalance(false, fundAmount); @@ -408,10 +407,10 @@ contract Clearinghouse is Policy, RolesConsumer, CoolerCallback { emit Defund(address(token_), amount_); } - /// @notice Burn any gOHM defaulted using the Cooler instead of the Clearinghouse. - function burn() external { + /// @notice Public function to burn gOHM. + /// @dev Can be used to burn any gOHM defaulted using the Cooler instead of the Clearinghouse. + function burn() public { uint256 gohmBalance = gohm.balanceOf(address(this)); - // Unstake and burn gOHM holdings. gohm.approve(address(staking), gohmBalance); MINTR.burnOhm(address(this), staking.unstake(address(this), gohmBalance, false, false)); diff --git a/src/test/Clearinghouse.t.sol b/src/test/Clearinghouse.t.sol index 2af48ab..469d6bd 100644 --- a/src/test/Clearinghouse.t.sol +++ b/src/test/Clearinghouse.t.sol @@ -325,7 +325,7 @@ contract ClearinghouseTest is Test { uint256 initInterest = clearinghouse.interestReceivables(); uint256 initPrincipal = clearinghouse.principalReceivables(); // Approve the interest of the extensions - uint256 interestOwed = clearinghouse.interestForLoan(initLoan.principal, initLoan.request.duration * times_); + uint256 interestOwed = clearinghouse.interestForLoan(initLoan.principal, initLoan.request.duration) * times_; dai.approve(address(clearinghouse), interestOwed); // Extend loan clearinghouse.extendLoan(cooler, loanID, times_); @@ -346,7 +346,7 @@ contract ClearinghouseTest is Test { assertEq(clearinghouse.principalReceivables(), initPrincipal); } - function testFuzz_extendLoan_withPriorRepayment(uint256 loanAmount_, uint256 elapsed_, uint8 times_) public { + function testFuzz_extendLoan_withPriorRepayment(uint256 loanAmount_, uint256 repay_, uint256 elapsed_, uint8 times_) public { // Loan amount cannot exceed Clearinghouse funding loanAmount_ = bound(loanAmount_, 1e10, clearinghouse.FUND_AMOUNT()); elapsed_ = bound(elapsed_, 0, clearinghouse.DURATION()); @@ -358,18 +358,22 @@ contract ClearinghouseTest is Test { // Move forward without defaulting _skip(elapsed_); - vm.startPrank(user); - dai.approve(address(cooler), initLoan.interestDue); + // Bound repayment + repay_ = bound(elapsed_, 1, initLoan.interestDue); + // Repay interest of the first extension manually - cooler.repayLoan(loanID, initLoan.interestDue); + vm.startPrank(user); + dai.approve(address(cooler), repay_); + cooler.repayLoan(loanID, repay_); // Cache DAI balance and interest after repayment + Cooler.Loan memory repaidLoan = cooler.getLoan(loanID); uint256 initDaiUser = dai.balanceOf(user); uint256 initDaiCH = dai.balanceOf(address(clearinghouse)); uint256 initInterest = clearinghouse.interestReceivables(); uint256 initPrincipal = clearinghouse.principalReceivables(); // Approve the interest of the followup extensions - uint256 interestOwed = clearinghouse.interestForLoan(initLoan.principal, initLoan.request.duration * (times_ - 1)); + uint256 interestOwed = clearinghouse.interestForLoan(initLoan.principal, initLoan.request.duration) * (times_ - 1) + repaidLoan.interestDue; dai.approve(address(clearinghouse), interestOwed); // Extend loan @@ -387,7 +391,7 @@ contract ClearinghouseTest is Test { assertEq(extendedLoan.collateral, initLoan.collateral, "collateral"); assertEq(extendedLoan.expiry, initLoan.expiry + initLoan.request.duration * times_, "expiry"); // Check: clearinghouse storage - assertEq(clearinghouse.interestReceivables(), initInterest + extendedLoan.interestDue); + assertEq(clearinghouse.interestReceivables(), initInterest + extendedLoan.interestDue - repaidLoan.interestDue); assertEq(clearinghouse.principalReceivables(), initPrincipal); }