Skip to content

Commit

Permalink
feat: late fee imposing, transfer consolidation, new view functions (#45
Browse files Browse the repository at this point in the history
)

* feat: introduce imposing of a late fee
* fix: code size optimization (-0.54 kB)
* style: fix formatting issues
* test: cover the fine logic by tests
* fix: remove redundant import in the lending market contract
* fix: move function "addonTreasury()" to the ILiquidityPool interface
* feat: consolidate token transfers during loan taking and revocation

All transfers are currently executed by the lending market contract only.
Previously some of them were executed by the liquidity pool contracts.
In the case of multiple sub-loans all similar transfers are united into a single one.

* test: cover the mew logic by tests
* docs: describe internal functions of the lending market contract
* feat: introduce the extended preview for loans
* feat: remove functions "getLoanStateBatch()", "getLoanPreviewBatch()"
* test: cover new fields of the loan preview structures by tests
* test: minor test improvements
* style: fix formatting issues in smart-contracts
* fix: replace "instalment" (single "l") => "installment" (double "l")
* feat: update contracts version to `1.5.0`
  • Loading branch information
EvgeniiZaitsevCW authored Dec 27, 2024
1 parent b756a21 commit 08d1a55
Show file tree
Hide file tree
Showing 20 changed files with 1,703 additions and 1,072 deletions.
334 changes: 224 additions & 110 deletions contracts/LendingMarket.sol

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion contracts/common/Versionable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ abstract contract Versionable is IVersionable {
* @inheritdoc IVersionable
*/
function $__VERSION() external pure returns (Version memory) {
return Version(1, 4, 0);
return Version(1, 5, 0);
}
}
1 change: 1 addition & 0 deletions contracts/common/interfaces/ICreditLineConfigurable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ interface ICreditLineConfigurable is ICreditLine {
uint32 maxAddonFixedRate; // The maximum fixed rate for the loan addon calculation.
uint32 minAddonPeriodRate; // The minimum period rate for the loan addon calculation.
uint32 maxAddonPeriodRate; // The maximum period rate for the loan addon calculation.
uint32 lateFeeRate; // The late fee rate to be applied to the loan.
}

/// @dev A struct that defines borrower configuration.
Expand Down
8 changes: 0 additions & 8 deletions contracts/common/interfaces/ILiquidityPoolAccountable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,4 @@ interface ILiquidityPoolAccountable is ILiquidityPool {
/// @param account The address of the account to check.
/// @return True if the account is configured as an admin.
function isAdmin(address account) external view returns (bool);

/// @dev Returns the addon treasury address.
///
/// If the address is zero the addon amount of a loan is retained in the pool.
/// Otherwise the addon amount transfers to that treasury when a loan is taken and back when a loan is revoked.
///
/// @return The current address of the addon treasury.
function addonTreasury() external view returns (address);
}
3 changes: 3 additions & 0 deletions contracts/common/interfaces/core/ICreditLine.sol
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ interface ICreditLine {
/// @dev Returns the address of the credit line token.
function token() external view returns (address);

/// @dev Returns the late fee rate to be applied to loans taken out with this credit line.
function lateFeeRate() external view returns (uint256);

/// @dev Proves the contract is the credit line one. A marker function.
function proveCreditLine() external pure;
}
13 changes: 4 additions & 9 deletions contracts/common/interfaces/core/ILendingMarket.sol
Original file line number Diff line number Diff line change
Expand Up @@ -318,25 +318,20 @@ interface ILendingMarket {
/// @return The stored state of the loan (see the `Loan.State` struct).
function getLoanState(uint256 loanId) external view returns (Loan.State memory);

/// @dev Gets the stored state of a batch of ordinary loans or sub-loans.
/// @param loanIds The unique identifiers of the loans to check.
/// @return The stored states of the loans (see the `Loan.State` struct).
function getLoanStateBatch(uint256[] calldata loanIds) external view returns (Loan.State[] memory);

/// @dev Gets the preview of an ordinary loan or a sub-loan at a specific timestamp.
/// @param loanId The unique identifier of the loan to check.
/// @param timestamp The timestamp to get the loan preview for.
/// @return The preview state of the loan (see the `Loan.Preview` struct).
function getLoanPreview(uint256 loanId, uint256 timestamp) external view returns (Loan.Preview memory);

/// @dev Gets the loan preview at a specific timestamp for a batch of ordinary loans or sub-loans.
/// @dev Gets the loan extended preview at a specific timestamp for a batch of ordinary loans or sub-loans.
/// @param loanIds The unique identifiers of the loans to check.
/// @param timestamp The timestamp to get the loan preview for. If 0, the current timestamp is used.
/// @return The preview states of the loans (see the `Loan.Preview` struct).
function getLoanPreviewBatch(
/// @return The extended previews of the loans (see the `Loan.PreviewExtended` struct).
function getLoanPreviewExtendedBatch(
uint256[] calldata loanIds,
uint256 timestamp
) external view returns (Loan.Preview[] memory);
) external view returns (Loan.PreviewExtended[] memory);

/// @dev Gets the preview of an installment loan at a specific timestamp.
///
Expand Down
8 changes: 8 additions & 0 deletions contracts/common/interfaces/core/ILiquidityPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ interface ILiquidityPool {
/// @dev Returns the address of the liquidity pool token.
function token() external view returns (address);

/// @dev Returns the addon treasury address.
///
/// If the address is zero the addon amount of a loan is retained in the pool.
/// Otherwise the addon amount transfers to that treasury when a loan is taken and back when a loan is revoked.
///
/// @return The current address of the addon treasury.
function addonTreasury() external view returns (address);

/// @dev Proves the contract is the liquidity pool one. A marker function.
function proveLiquidityPool() external pure;
}
75 changes: 67 additions & 8 deletions contracts/common/libraries/Loan.sol
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,13 @@ library Loan {
uint32 trackedTimestamp; // The timestamp when the loan was last paid or its balance was updated.
uint32 freezeTimestamp; // The timestamp when the loan was frozen. Zero value for unfrozen loans.
uint40 firstInstallmentId; // The ID of the first installment for sub-loans or zero for ordinary loans.
uint8 instalmentCount; // The total number of installments for sub-loans or zero for ordinary loans.
uint8 installmentCount; // The total number of installments for sub-loans or zero for ordinary loans.
// uint16 __reserved; // Reserved for future use.
// Slot 5
uint64 lateFeeAmount; // The late fee amount of the loan or zero if the loan is not defaulted.
}

/// @dev A struct that defines the terms of the loan.
/// @dev A struct that defines the terms of a loan.
struct Terms {
// Slot 1
address token; // The address of the token to be used for the loan.
Expand All @@ -61,36 +63,93 @@ library Loan {
uint256 outstandingBalance; // The outstanding balance of the loan at the previewed period.
}

/// @dev A struct that defines the extended preview of a loan.
///
/// Fields:
/// - periodIndex ------------ The period index that matches the preview timestamp.
/// - trackedBalance --------- The tracked balance of the loan at the previewed period.
/// - outstandingBalance ----- The outstanding balance of the loan at the previewed period.
/// - borrowAmount ----------- The borrow amount of the loan at the previewed period.
/// - addonAmount ------------ The addon amount of the loan at the previewed period.
/// - repaidAmount ----------- The repaid amount of the loan at the previewed period.
/// - lateFeeAmount ---------- The late fee amount of the loan at the previewed period.
/// - programId -------------- The program ID of the loan.
/// - borrower --------------- The borrower of the loan.
/// - previewTimestamp ------- The preview timestamp.
/// - startTimestamp --------- The start timestamp of the loan.
/// - trackedTimestamp ------- The tracked timestamp of the loan.
/// - freezeTimestamp -------- The freeze timestamp of the loan.
/// - durationInPeriods ------ The duration in periods of the loan.
/// - interestRatePrimary ---- The primary interest rate of the loan.
/// - interestRateSecondary -- The secondary interest rate of the loan.
/// - firstInstallmentId ----- The ID of the first installment for sub-loans or zero for ordinary loans.
/// - installmentCount ------- The total number of installments for sub-loans or zero for ordinary loans.
struct PreviewExtended {
uint256 periodIndex;
uint256 trackedBalance;
uint256 outstandingBalance;
uint256 borrowAmount;
uint256 addonAmount;
uint256 repaidAmount;
uint256 lateFeeAmount;
uint256 programId;
address borrower;
uint256 previewTimestamp;
uint256 startTimestamp;
uint256 trackedTimestamp;
uint256 freezeTimestamp;
uint256 durationInPeriods;
uint256 interestRatePrimary;
uint256 interestRateSecondary;
uint256 firstInstallmentId;
uint256 installmentCount;
}

/// @dev A struct that defines the preview of an installment loan.
///
/// The structure can be returned for both ordinary and installment loans.
///
/// The purpose of the fields in the case of installment loans:
///
/// - firstInstallmentId ------- The first installment ID.
/// - instalmentCount ---------- The total number of installments.
/// - installmentCount --------- The total number of installments.
/// - periodIndex -------------- The period index that matches the preview timestamp.
/// - totalTrackedBalance ------ The total tracked balance of all installments.
/// - totalOutstandingBalance -- The total outstanding balance of all installments.
/// - totalOutstandingBalance -- The total outstanding balance of all installments
/// - totalBorrowAmount -------- The total borrow amount of all installments.
/// - totalAddonAmount --------- The total addon amount of all installments.
/// - totalRepaidAmount -------- The total repaid amount of all installments.
/// - totalLateFeeAmount ------- The total late fee amount of all installments.
/// - installmentPreviews ------ The extended previews of all installments.
///
/// The purpose of the fields in the case of ordinary loans:
///
/// - firstInstallmentId ------- The ID of the loan.
/// - instalmentCount ---------- The total number of installments that always equals zero.
/// - installmentCount --------- The total number of installments that always equals zero.
/// - periodIndex -------------- The period index that matches the preview timestamp.
/// - totalTrackedBalance ------ The tracked balance of the loan.
/// - totalOutstandingBalance -- The outstanding balance of the loan.
///
/// - totalBorrowAmount -------- The borrow amount of the loan.
/// - totalAddonAmount --------- The addon amount of the loan.
/// - totalRepaidAmount -------- The repaid amount of the loan.
/// - totalLateFeeAmount ------- The late fee amount of the loan.
/// - installmentPreviews ------ The extended preview of the loan as a single item array.

/// Notes:
///
/// 1. The `totalTrackedBalance` fields calculates as the sum of tracked balances of all installments.
/// 2. The `totalOutstandingBalance` fields calculates as the sum of rounded tracked balances
/// of all installments according to the `ACCURACY_FACTOR` constant.
struct InstallmentLoanPreview {
uint256 firstInstallmentId;
uint256 instalmentCount;
uint256 firstInstallmentId;
uint256 installmentCount;
uint256 periodIndex;
uint256 totalTrackedBalance;
uint256 totalOutstandingBalance;
uint256 totalBorrowAmount;
uint256 totalAddonAmount;
uint256 totalRepaidAmount;
uint256 totalLateFeeAmount;
PreviewExtended[] installmentPreviews;
}
}
5 changes: 5 additions & 0 deletions contracts/credit-lines/CreditLineConfigurable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,11 @@ contract CreditLineConfigurable is
return _token;
}

/// @inheritdoc ICreditLine
function lateFeeRate() external view returns (uint256) {
return _config.lateFeeRate;
}

/// @dev Calculates the amount of a loan addon (extra charges or fees).
/// @param amount The initial principal amount of the loan.
/// @param durationInPeriods The duration of the loan in periods.
Expand Down
20 changes: 7 additions & 13 deletions contracts/liquidity-pools/LiquidityPoolAccountable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ contract LiquidityPoolAccountable is
/// an incorrect value of the `_addonsBalance` variable, or a reversion if `_addonsBalance == 0`.
error AddonTreasuryAddressZeroingProhibited();

/// @dev Thrown when the addon treasury has not provided an allowance for the pool contract to transfer its tokens.
error AddonTreasuryZeroAllowance();
/// @dev Thrown when the addon treasury has not provided an allowance for the lending market to transfer its tokens.
error AddonTreasuryZeroAllowanceForMarket();

/// @dev Thrown when the token source balance is insufficient.
error InsufficientBalance();
Expand Down Expand Up @@ -309,7 +309,7 @@ contract LiquidityPoolAccountable is
return _token;
}

/// @dev ILiquidityPool
/// @inheritdoc ILiquidityPool
function addonTreasury() external view returns (address) {
return _addonTreasury;
}
Expand All @@ -334,8 +334,8 @@ contract LiquidityPoolAccountable is
if (newTreasury == address(0)) {
revert AddonTreasuryAddressZeroingProhibited();
}
if (IERC20(_token).allowance(newTreasury, address(this)) == 0) {
revert AddonTreasuryZeroAllowance();
if (IERC20(_token).allowance(newTreasury, _market) == 0) {
revert AddonTreasuryZeroAllowanceForMarket();
}
emit AddonTreasuryChanged(newTreasury, oldTreasury);
_addonTreasury = newTreasury;
Expand All @@ -345,23 +345,17 @@ contract LiquidityPoolAccountable is
///
/// See the comments of the {_addonTreasury} storage variable for more details.
function _collectLoanAddon(uint64 addonAmount) internal {
address addonTreasury_ = _addonTreasury;
if (addonTreasury_ == address(0)) {
if (_addonTreasury == address(0)) {
_addonsBalance += addonAmount;
} else {
IERC20(_token).safeTransfer(addonTreasury_, addonAmount);
}
}

/// @dev Revokes the addon amount depending on the addon treasury address.
///
/// See the comments of the {_addonTreasury} storage variable for more details.
function _revokeLoanAddon(uint64 addonAmount) internal {
address addonTreasury_ = _addonTreasury;
if (addonTreasury_ == address(0)) {
if (_addonTreasury == address(0)) {
_addonsBalance -= addonAmount;
} else {
IERC20(_token).safeTransferFrom(addonTreasury_, address(this), addonAmount);
}
}
}
10 changes: 10 additions & 0 deletions contracts/mocks/CreditLineMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ contract CreditLineMock is ICreditLine {

bool private _onAfterLoanRevocationResult;

uint256 private _lateFeeRate;

// -------------------------------------------- //
// ICreditLine functions //
// -------------------------------------------- //
Expand Down Expand Up @@ -75,6 +77,10 @@ contract CreditLineMock is ICreditLine {
return _tokenAddress;
}

function lateFeeRate() external view returns (uint256) {
return _lateFeeRate;
}

// -------------------------------------------- //
// Mock functions //
// -------------------------------------------- //
Expand All @@ -88,5 +94,9 @@ contract CreditLineMock is ICreditLine {
_loanTerms[borrower] = terms;
}

function mockLateFeeRate(uint256 newRate) external {
_lateFeeRate = newRate;
}

function proveCreditLine() external pure {}
}
9 changes: 2 additions & 7 deletions contracts/mocks/LendingMarketMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -176,21 +176,16 @@ contract LendingMarketMock is ILendingMarket {
return _loanStates[loanId];
}

function getLoanStateBatch(uint256[] calldata loanIds) external pure returns (Loan.State[] memory) {
loanIds; // To prevent compiler warning about unused variable
revert Error.NotImplemented();
}

function getLoanPreview(uint256 loanId, uint256 timestamp) external pure returns (Loan.Preview memory) {
loanId; // To prevent compiler warning about unused variable
timestamp; // To prevent compiler warning about unused variable
revert Error.NotImplemented();
}

function getLoanPreviewBatch(
function getLoanPreviewExtendedBatch(
uint256[] calldata loanIds,
uint256 timestamp
) external pure returns (Loan.Preview[] memory) {
) external pure returns (Loan.PreviewExtended[] memory) {
loanIds; // To prevent compiler warning about unused variable
timestamp; // To prevent compiler warning about unused variable
revert Error.NotImplemented();
Expand Down
10 changes: 10 additions & 0 deletions contracts/mocks/LiquidityPoolMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ contract LiquidityPoolMock is ILiquidityPool {

bool private _onAfterLoanRevocationResult;

address private _addonTreasury;

// -------------------------------------------- //
// ILiquidityPool functions //
// -------------------------------------------- //
Expand Down Expand Up @@ -64,6 +66,10 @@ contract LiquidityPoolMock is ILiquidityPool {
return _tokenAddress;
}

function addonTreasury() external view returns (address) {
return _addonTreasury;
}

// -------------------------------------------- //
// Mock functions //
// -------------------------------------------- //
Expand All @@ -76,6 +82,10 @@ contract LiquidityPoolMock is ILiquidityPool {
IERC20(token_).approve(_market, type(uint56).max);
}

function mockAddonTreasury(address newTreasury) external {
_addonTreasury = newTreasury;
}

function proveLiquidityPool() external pure {}

function repayLoan(address _market, uint256 loanId, uint256 amount) external {
Expand Down
12 changes: 11 additions & 1 deletion test-utils/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,23 @@ import { expect } from "chai";
import { network } from "hardhat";
import { loadFixture } from "@nomicfoundation/hardhat-network-helpers";

export function checkEquality<T extends Record<string, unknown>>(actualObject: T, expectedObject: T, index?: number) {
export function checkEquality<T extends Record<string, unknown>>(
actualObject: T,
expectedObject: T,
index?: number,
props: {
ignoreObjects: boolean;
} = { ignoreObjects: false }
) {
const indexString = index == null ? "" : ` with index: ${index}`;
Object.keys(expectedObject).forEach(property => {
const value = actualObject[property];
if (typeof value === "undefined" || typeof value === "function") {
throw Error(`Property "${property}" is not found in the actual object` + indexString);
}
if (typeof expectedObject[property] === "object" && props.ignoreObjects) {
return;
}
expect(value).to.eq(
expectedObject[property],
`Mismatch in the "${property}" property between the actual object and expected one` + indexString
Expand Down
9 changes: 9 additions & 0 deletions test-utils/eth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,12 @@ export async function increaseBlockTimestampTo(target: number) {
throw new Error(`Setting block timestamp for the current blockchain is not supported: ${network.name}`);
}
}

export async function getNumberOfEvents(
tx: TransactionResponse | Promise<TransactionResponse>,
contract: Contract,
eventName: string
): Promise<number> {
const topic = contract.filters[eventName].fragment.topicHash;
return (await proveTx(tx)).logs.filter(log => log.topics[0] == topic).length;
}
Loading

0 comments on commit 08d1a55

Please # to comment.