Old Obsidian Nuthatch
High
Borrower can bypass the condition checks during matching using reentrancy.
- The DebitaV3Aggregator.matchOffersV3() function checks the borrow offer's conditions (e.g., duration) against those of lend offers before calling
DBOImplementation.acceptBorrowOffer()
and finally creating a loan with the borrow offer's condition.
function matchOffersV3(
address[] memory lendOrders,
uint[] memory lendAmountPerOrder,
uint[] memory porcentageOfRatioPerLendOrder,
address borrowOrder,
address[] memory principles,
uint[] memory indexForPrinciple_BorrowOrder,
uint[] memory indexForCollateral_LendOrder,
uint[] memory indexPrinciple_LendOrder
) external nonReentrant returns (address) {
...... SKIP ......
// check that the duration is between the min and max duration from the lend order
require(
@> borrowInfo.duration >= lendInfo.minDuration &&
borrowInfo.duration <= lendInfo.maxDuration,
"Invalid duration"
);
...... SKIP ......
@> DBOImplementation(borrowOrder).acceptBorrowOffer(
borrowInfo.isNFT ? 1 : amountOfCollateral
);
...... SKIP ......
DebitaV3Loan deployedLoan = DebitaV3Loan(address(_loanProxy));
// init loan
deployedLoan.initialize(
borrowInfo.collateral,
principles,
borrowInfo.isNFT,
borrowInfo.receiptID,
borrowInfo.isNFT ? 1 : amountOfCollateral,
borrowInfo.valuableAssetAmount,
amountOfCollateral,
borrowInfo.valuableAsset,
@> borrowInfo.duration,
amountPerPrinciple,
borrowID, //borrowInfo.id,
offers,
s_OwnershipContract,
feeInterestLender,
feeAddress
);
...... SKIP ......
}
- The DBOImplementation.acceptBorrowOffer() function transfers collaterals from the contract to the borrower when the residual collateral is less than 10 percent and not zero.
function acceptBorrowOffer(
uint amount
) public onlyAggregator nonReentrant onlyAfterTimeOut {
...... SKIP ......
uint percentageOfAvailableCollateral = (borrowInformation
.availableAmount * 10000) / m_borrowInformation.startAmount;
// if available amount is less than 0.1% of the start amount, the order is no longer active and will count as completed.
if (percentageOfAvailableCollateral <= 10) {
isActive = false;
// transfer remaining collateral back to owner
if (borrowInformation.availableAmount != 0) {
@> SafeERC20.safeTransfer(
IERC20(m_borrowInformation.collateral),
m_borrowInformation.owner,
borrowInformation.availableAmount
);
}
borrowInformation.availableAmount = 0;
IDBOFactory(factoryContract).emitDelete(address(this));
IDBOFactory(factoryContract).deleteBorrowOrder(address(this));
} else {
IDBOFactory(factoryContract).emitUpdate(address(this));
}
}
- As per readme, "any ERC20 that follows exactly the standard" can be used as collateral for borrow offers. However, if an ERC-777 token is used as collateral, the borrower's callback function will be triggered when the collaterals are transferred. At this point, the callback function can call the
DBOImplementation.updateBorrowOrder()
function. - The DBOImplementation.updateBorrowOrder() function does not have a
nonReentrant
modifier.
function updateBorrowOrder(
uint newMaxApr,
uint newDuration,
uint[] memory newLTVs,
uint[] memory newRatios
) public onlyOwner {
...... SKIP ......
}
- A borrower creates a borrow offer with an ERC-777 token as collateral.
No response
The updateBorrowOrder()
function can modify several parameters of the borrow offer, including APR, LTV, and duration. Here’s a simplified scenario focusing on the duration:
- The borrower creates a borrow offer using an ERC-777 token as collateral, with a duration of
10 days
. - The borrower matches their own borrow offer with lend offers by calling
DebitaV3Aggregator.matchOffersV3()
, where the lend offers have a duration range of5 to 20 days
. - The
matchOffersV3()
function verifies that the borrow offer's duration falls within the lend offers' duration range. - The
matchOffersV3()
function callsDBOImplementation.acceptBorrowOffer()
. - The
DBOImplementation.acceptBorrowOffer()
function transfers the collateral to the borrower. - Since the collateral is an ERC-777 token, the borrower’s callback function is invoked.
- The borrower’s callback function calls
DBOImplementation.updateBorrowOrder()
, changing the duration to30 days
. - As a result, the borrower is able to borrow funds for
30 days
, exceeding the maximum duration of20 days
set by the lend offers.
The borrower can manipulate key parameters, such as duration, APR, LTV, and ratios, to favorable borrowing terms that exceed the conditions agreed upon in the lending offers.
The updateBorrowOrder()
function can modify several parameters of the borrow offer, including APR, LTV, and duration. Here’s a simplified scenario focusing on the duration:
- The borrower creates a borrow offer using an ERC-777 token as collateral, with a duration of
10 days
. - The borrower matches their own borrow offer with lend offers by calling
DebitaV3Aggregator.matchOffersV3()
, where the lend offers have a duration range of5 to 20 days
. - The
matchOffersV3()
function verifies that the borrow offer's duration falls within the lend offers' duration range. - The
matchOffersV3()
function callsDBOImplementation.acceptBorrowOffer()
. - The
DBOImplementation.acceptBorrowOffer()
function transfers the collateral to the borrower. - Since the collateral is an ERC-777 token, the borrower’s callback function is invoked.
- The borrower’s callback function calls
DBOImplementation.updateBorrowOrder()
, changing the duration to30 days
. - As a result, the borrower is able to borrow funds for
30 days
, exceeding the maximum duration of20 days
set by the lend offers.
Add the nonReentrant
modifier to the DBOImplementation.updateBorrowOrder()
function as follows.
function updateBorrowOrder(
uint newMaxApr,
uint newDuration,
uint[] memory newLTVs,
uint[] memory newRatios
-- ) public onlyOwner {
++ ) public onlyOwner nonReentrant {
...... SKIP ......
}