Skip to content
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
1,026 changes: 1,026 additions & 0 deletions contracts/capybara-finance/.openzeppelin/unknown-2008.json

Large diffs are not rendered by default.

1,048 changes: 1,037 additions & 11 deletions contracts/capybara-finance/.openzeppelin/unknown-2009.json

Large diffs are not rendered by default.

13 changes: 12 additions & 1 deletion contracts/capybara-finance/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
# Unreleased
# v1.24.0

## Main Changes

Fixed the `_revokeLoan()` internal function to skip the `onAfterLoanRevocation()` credit line hook for already repaid loans (where `trackedBalance == 0`). Previously, revoking an installment loan that contained fully repaid sub-loans would still call the credit line hook for those sub-loans, incorrectly decrementing the borrower's `activeLoanCount` and `totalActiveLoanAmount` in the credit line state. Now the hook is only called for sub-loans that have an outstanding balance at the time of revocation.

## Migration

1. No special actions required, just upgrade the deployed `CapybaraFinance` smart-contracts.
2. To be sure there are no broken borrower states on existing credit lines, you can check all the cases of loan revocation through the block explorer database. Only installment loan revocation cases should be checked through event `InstallmentLoanRevoked` because fully repaid ordinary loans cannot be revoked. The installment loans with a single installment can be skipped as well.

# v1.23.1

## Main Changes

Expand Down
5 changes: 4 additions & 1 deletion contracts/capybara-finance/contracts/LendingMarket.sol
Original file line number Diff line number Diff line change
Expand Up @@ -774,11 +774,14 @@ contract LendingMarket is
*/
function _revokeLoan(uint256 loanId, Loan.State storage loan) internal {
address creditLine = _programCreditLines[loan.programId];
bool isLoanRepaid = _isRepaid(loan);

loan.trackedBalance = 0;
loan.trackedTimestamp = _blockTimestamp().toUint32();

ICreditLine(creditLine).onAfterLoanRevocation(loanId);
if (!isLoanRepaid) {
ICreditLine(creditLine).onAfterLoanRevocation(loanId);
}

emit LoanRevoked(loanId);
}
Expand Down
2 changes: 1 addition & 1 deletion contracts/capybara-finance/contracts/Versionable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ import { IVersionable } from "@cloudwalk/brlc-base/interfaces/IVersionable.sol";
abstract contract Versionable is IVersionable {
/// @inheritdoc IVersionable
function $__VERSION() external pure returns (Version memory) {
return Version(1, 23, 0);
return Version(1, 24, 0);
}
}
2 changes: 1 addition & 1 deletion contracts/capybara-finance/test-utils/specific.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ export interface Version {

export const EXPECTED_VERSION: Version = {
major: 1,
minor: 23,
minor: 24,
patch: 0,
};
18 changes: 12 additions & 6 deletions contracts/capybara-finance/test/LendingMarket.base.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4741,6 +4741,8 @@ describe("Contract 'LendingMarket': base tests", () => {
const { market, installmentLoanParts: loans } = fixture;
const loanIds = loans.map(loan => loan.id);
const expectedLoans = props.loans.map(loan => clone(loan));
const loanRepaidStatesBeforeRevocation = expectedLoans.map(loan => loan.state.trackedBalance == 0);
const areAllLoansRepaidBeforeRevocation = loanRepaidStatesBeforeRevocation.every(isRepaid => isRepaid);
const borrowerBalanceChange = expectedLoans
.map(loan => loan.state.repaidAmount - loan.state.borrowedAmount)
.reduce((sum, amount) => sum + amount);
Expand All @@ -4756,19 +4758,23 @@ describe("Contract 'LendingMarket': base tests", () => {

const revocationTimestamp = calculateTimestampWithOffset(await getTxTimestamp(tx));
const actualLoanStates = await getLoanStates(market, loanIds);
expectedLoans.forEach((loan) => {
loan.state.trackedBalance = 0;
loan.state.trackedTimestamp = revocationTimestamp;
});

for (let i = 0; i < loanIds.length; ++i) {
const loanId = loanIds[i];
await expect(tx).to.emit(market, EVENT_NAME_LOAN_REVOKED).withArgs(loanId);
const loan = expectedLoans[i];
loan.state.trackedBalance = 0;
loan.state.trackedTimestamp = revocationTimestamp;
checkEquality(actualLoanStates[i], expectedLoans[i].state, i);
await expect(tx).to.emit(market, EVENT_NAME_LOAN_REVOKED).withArgs(loanId);
// Check hook calls
await expect(tx).to.emit(creditLine, EVENT_NAME_ON_AFTER_LOAN_REVOCATION_CALLED).withArgs(loanId);
if (!loanRepaidStatesBeforeRevocation[i]) {
await expect(tx).to.emit(creditLine, EVENT_NAME_ON_AFTER_LOAN_REVOCATION_CALLED).withArgs(loanId);
}
}
await expect(tx).to.emit(market, EVENT_NAME_INSTALLMENT_LOAN_REVOKED).withArgs(loanIds[0], loanIds.length);
if (areAllLoansRepaidBeforeRevocation) {
await expect(tx).not.to.emit(creditLine, EVENT_NAME_ON_AFTER_LOAN_REVOCATION_CALLED);
}

const totalAddonAmount = expectedLoans
.map(loan => loan.state.addonAmount)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -524,7 +524,7 @@ describe("Contract 'LendingMarket': complex tests", () => {
}

async function executeAndCheckLoanRevocationForScenario(context: TestScenarioContext) {
const { token, lendingMarket, liquidityPool } = context.fixture as Fixture;
const { token, lendingMarket, liquidityPool, creditLine } = context.fixture as Fixture;
const scenario = context.scenario;
const loanState = await lendingMarket.getLoanState(context.loanId);
const refundAmount = Number(loanState.repaidAmount) - scenario.borrowedAmount;
Expand All @@ -541,9 +541,12 @@ describe("Contract 'LendingMarket': complex tests", () => {
);

const liquidityPoolBalancesAfter = await liquidityPool.getBalances();
const borrowerStateAfter = await creditLine.getBorrowerState(borrower.address);
expect(liquidityPoolBalancesAfter[0])
.to.eq(Number(liquidityPoolBalancesBefore[0]) - refundAmount + scenario.addonAmount);
expect(liquidityPoolBalancesAfter[1]).to.eq(0); // The addonsBalance must be zero because addonTreasury != 0
expect(borrowerStateAfter.activeLoanCount).to.eq(0);
expect(borrowerStateAfter.totalActiveLoanAmount).to.eq(0);

await checkLoanRepaidAmountForScenario(context);
await checkLoanClosedState(lendingMarket, context.loanId);
Expand Down