Matched orders removed from the tree without being recorded when their count exceeds MAX_MATCH_DETAILS
Description
The function _matchOrder
searches for matching orders in the specified tree based on the given parameters, and returns a struct MatchedOrder
, which records the detailed information of the matched orders (counterParty
and amounts
), the total loan token amount of the matched orders (totalMatched
), and the total number of matched orders (totalCount
).
struct MatchedOrder {
address[] counterParty;
uint256[] amounts;
uint256 totalMatched;
uint256 totalCount;
}
When an order is matched, the function _matchOrder
removes it from the tree. However, if the number of matched orders exceeds MAX_MATCH_DETAILS
, the arrays recording the detailed information of matched orders will no longer be updated, though the matched orders will still be removed from the tree.
function _matchOrder(RedBlackTreeLib.Tree storage tree, bool isLender, uint64 rate, uint64 ltv, uint256 amount)
internal
returns (MatchedOrder memory matchedOrders)
{
if (amount == 0) revert ErrorsLib.InvalidInput();
matchedOrders.counterParty = new address[](MAX_MATCH_DETAILS);
matchedOrders.amounts = new uint256[](MAX_MATCH_DETAILS);
uint256 matchedCount = 0;
uint256 remaining = amount;
bytes32 currentPtr = tree.first();
while (remaining > 0 && currentPtr != bytes32(0)) {
// [...]
if (_isMatchingOrder(currentPtr, rate, ltv, isLender)) {
// [...]
while (i < entriesCount && remaining > 0) {
RedBlackTreeLib.Entry storage entry = tree.getEntryAt(compositeKey, i);
uint256 fill = entry.amount > remaining ? remaining : entry.amount;
if (recordDetails) {
matchedOrders.counterParty[matchedCount] = entry.account;
matchedOrders.amounts[matchedCount] = fill;
matchedCount++;
matchedOrders.totalCount++;
recordDetails = matchedCount < MAX_MATCH_DETAILS;
}
matchedOrders.totalMatched += fill;
remaining -= fill;
// [...]
entry.amount = entry.amount - fill;
if (entry.amount == 0) {
tree.removeEntry(compositeKey, i);
// [...]
} else {
i++;
tree._heapifyDown(compositeKey, i);
}
}
}
// [...]
}
}
Impact
In actual usage, based on the result returned by the function _matchOrder
, the unique pool addresses involved in the array counterParty
are aggregated, and the loan token amounts of orders belonging to the same pool are summed. Then, according to the required collateral token amounts returned by each pool's previewBorrow
function, the functions depositCollateral
and borrow
of each pool are called in sequence.
If the actual number of orders actually matched by the function _matchOrder
is greater than MAX_MATCH_DETAILS
, there may be a pool to which the matched order belongs to does not overlap with the pools in the array counterParty
. The matched orders from this unrecorded pool will be removed from the lenderTree
, while the corresponding pool's borrow
function will not be invoked by the contract Orderbook during this transaction. As a result, the pool will not be aware that its orders have been matched and thus cannot update its orders in the contract Orderbook in time.
function matchMarketBorrowOrder(uint256 amount, uint256 minAmountExpected, uint256 collateralBuffer, uint64 ltv, uint64 rate)
external
nonReentrant
whenNotPaused
{
// [...]
MatchedOrder memory matchedOrder = lenderTree._matchOrder(false, rate, ltv, amount);
uint256 amountReceived;
if (matchedOrder.totalMatched > 0) {
// [...]
// 1. Aggregate amounts by pool (O(n^2) but acceptable for small n)
(poolData, uniquePoolCount) = _aggregatePoolData(matchedOrder);
// 2. Update collateral fields in poolData using the helper function
totalCollateral = _calculateAndSetPoolCollateral(poolData, uniquePoolCount, msg.sender, collateralBuffer);
// [...]
// 5. Process all pools
amountReceived = _processPoolMatches(poolData, uniquePoolCount, msg.sender, minAmountExpected);
}
}
Recommendations
Consider ending the matching process when the number of matches reaches MAX_MATCH_DETAILS
.
Remediation
This issue has been acknowledged by AVON TECH LTD, and fixes were implemented in the following commits: