Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: NO full interest on partially repaid extensions #61

Merged
merged 1 commit into from
Sep 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 14 additions & 15 deletions src/Clearinghouse.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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 -----------------------------------------------------
Expand Down Expand Up @@ -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.
Expand All @@ -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);

Expand Down Expand Up @@ -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));
Expand Down
18 changes: 11 additions & 7 deletions src/test/Clearinghouse.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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_);
Expand All @@ -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());
Expand All @@ -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
Expand All @@ -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);
}

Expand Down