matchProposals function will revert while a proposal (borrowRequest/loanOffer) has less than 10% remaining value.
Summary
According to the README:
'''
Proposals can be matched against each other, instead of just asking the takers to accept the offers so that the best deals can be made.
'''
As proposals can never be allocated inside matchProposals function while one of them has less than 10% fulfill amount remaining, then the function becomes useless and then you will need to do it manually.
Root Cause
In PredictDotLoan.sol there's a function called _assertFulfillAmountNotTooLow that checks if the amount fulfilled is too low.
/**
* @dev We want to ensure each fulfillment is at least 10% of the loan amount
* to prevent too many small loans from being created. It would be detrimental
* to user experience.
*
* If the remaining loan amount is less than 10% of the loan amount, we allow the fulfillment to be smaller than 10%
* but the loan has to be fully fulfilled.
*/
function _assertFulfillAmountNotTooLow(
uint256 fulfillAmount,
uint256 fulfilledAmount,
uint256 loanAmount
) private pure {
if (fulfillAmount != loanAmount - fulfilledAmount) {
if (fulfillAmount < loanAmount / 10) {
revert FulfillAmountTooLow();
}
}
}
As this function is done in a way that when the amount is less than 10% of the amount fulfilled of one proposal, it passes with that same proposal, but not the other. The other proposal has different fulfillment so it reverts with FulfillAmountTooLow().
Internal pre-conditions
Make one borrow request with some positionID
Make one loan offer with the same positionID.
Fill one of those proposals with >90% of the fulfillment.
External pre-conditions
No response
Attack Path
Try to match the proposals by calling matchProposals.
Impact
Can never match this proposals through matchProposals.
Not according to the README.
PoC
borrowRequest proposal scenario.
function test_matchProposals_revertWhenFulfillIslessThanTenPercentOfBorrowRequest() public {
IPredictDotLoan.Proposal memory loanOffer = _generateLoanOffer(IPredictDotLoan.QuestionType.Binary);
IPredictDotLoan.Proposal memory borrowRequest = _generateBorrowRequest(IPredictDotLoan.QuestionType.Binary);
// Fulfill most of the amount offered
uint256 fulfillAmount = borrowRequest.loanAmount - (borrowRequest.loanAmount / 11);
vm.prank(lender);
predictDotLoan.acceptBorrowRequest(borrowRequest, fulfillAmount);
vm.expectRevert(IPredictDotLoan.FulfillAmountTooLow.selector);
predictDotLoan.matchProposals(borrowRequest, loanOffer);
}
loanOffer proposal scenario:
function test_matchProposals_revertWhenFulfillIslessThanTenPercentOfLoanOffer() public {
IPredictDotLoan.Proposal memory loanOffer = _generateLoanOffer(IPredictDotLoan.QuestionType.Binary);
IPredictDotLoan.Proposal memory borrowRequest = _generateBorrowRequest(IPredictDotLoan.QuestionType.Binary);
// Fulfill most of the amount offered
uint256 fulfillAmount = loanOffer.loanAmount - (loanOffer.loanAmount / 11);
vm.prank(borrower);
predictDotLoan.acceptLoanOffer(loanOffer, fulfillAmount);
vm.expectRevert(IPredictDotLoan.FulfillAmountTooLow.selector);
predictDotLoan.matchProposals(borrowRequest, loanOffer);
}
Mitigation
Check first if one of the proposals remaining fulfillment is less than 10% of the total. If yes, then avoid the checks _assertFulfillAmountNotTooLow of that proposal. But watch out by the storage manipulation on _updateFulfillment.
Fit Canvas Squid
Medium
matchProposals
function will revert while a proposal (borrowRequest/loanOffer
) has less than 10% remaining value.Summary
According to the
README
: ''' Proposals can be matched against each other, instead of just asking the takers to accept the offers so that the best deals can be made. ''' As proposals can never be allocated insidematchProposals
function while one of them has less than 10% fulfill amount remaining, then the function becomes useless and then you will need to do it manually.Root Cause
In
PredictDotLoan.sol
there's a function called_assertFulfillAmountNotTooLow
that checks if the amount fulfilled is too low.As this function is done in a way that when the amount is less than 10% of the amount fulfilled of one proposal, it passes with that same proposal, but not the other. The other proposal has different fulfillment so it reverts with
FulfillAmountTooLow()
.Internal pre-conditions
positionID
positionID
.>90%
of the fulfillment.External pre-conditions
No response
Attack Path
matchProposals
.Impact
matchProposals
.PoC
borrowRequest
proposal scenario.loanOffer proposal
scenario
:Mitigation
Check first if one of the proposals remaining fulfillment is less than 10% of the total. If yes, then avoid the checks
_assertFulfillAmountNotTooLow
of that proposal. But watch out by the storage manipulation on_updateFulfillment
.