Platform: Code4rena
Start Date: 05/04/2022
Pot Size: $30,000 USDC
Total HM: 10
Participants: 47
Period: 3 days
Judge: gzeon
Total Solo HM: 4
Id: 106
League: ETH
Rank: 2/47
Findings: 3
Award: $6,351.94
π Selected for report: 2
π Solo Findings: 1
π Selected for report: 0xDjango
6144.5095 USDC - $6,144.51
https://github.com/code-423n4/2022-04-backed/blob/e8015d7c4b295af131f017e646ba1b99c8f608f0/contracts/NFTLoanFacilitator.sol#L214-L221 https://github.com/code-423n4/2022-04-backed/blob/e8015d7c4b295af131f017e646ba1b99c8f608f0/contracts/NFTLoanFacilitator.sol#L230-L250
If borrower lends their own loan, they can repay and close the loan before ownership of the lend ticket is transferred to the new lender. The borrower will keep the NFT + loan amount + accrued interest.
This exploit requires that the loanAssetContractAddress
token transfers control to the receiver.
createLoan()
.lend()
, funding their own loan. The Borrower receives the lend ticket, and funds are transferred to themself.repayAndCloseLoan()
before the lend ticket is transferred to the new lender.The following code illustrates that the new lender sends funds to the original lender prior to receiving the lend ticket in return.
} else { ERC20(loan.loanAssetContractAddress).safeTransferFrom( msg.sender, currentLoanOwner, accumulatedInterest + previousLoanAmount ); } ILendTicket(lendTicketContract).loanFacilitatorTransfer(currentLoanOwner, sendLendTicketTo, loanId);
The original lender/borrower calls the following repayAndCloseLoan()
function so that they receive their collateral NFT from the protocol.
function repayAndCloseLoan(uint256 loanId) external override notClosed(loanId) { Loan storage loan = loanInfo[loanId]; uint256 interest = _interestOwed( loan.loanAmount, loan.lastAccumulatedTimestamp, loan.perAnumInterestRate, loan.accumulatedInterest ); address lender = IERC721(lendTicketContract).ownerOf(loanId); loan.closed = true; ERC20(loan.loanAssetContractAddress).safeTransferFrom(msg.sender, lender, interest + loan.loanAmount); IERC721(loan.collateralContractAddress).safeTransferFrom( address(this), IERC721(borrowTicketContract).ownerOf(loanId), loan.collateralTokenId ); emit Repay(loanId, msg.sender, lender, interest, loan.loanAmount); emit Close(loanId); }
Finally, the new lender receives the lend ticket that has no utility at this point. The borrower now possesses the NFT, original loan amount, and accrued interest.
Manual review.
Move the line to transfer the lend ticket to the new lender above the line to transfer to funds to the original lender. Or, use reentrancyGuard from OpenZeppelin to remove the risk of reentrant calls completely.
If desired, also require that the lender cannot be the same account as the borrower of a loan.
#0 - wilsoncusack
2022-04-07T16:40:55Z
borrower would need to convince lender to use an ERC20 with this malicious callback, but yes is legit
#1 - wilsoncusack
2022-04-07T16:47:12Z
malicious ERC20 -> transfers value to borrow ticket holder -> calls repay and close loan (would need funds available to do so, but still nets up)
#2 - wilsoncusack
2022-04-08T00:52:10Z
Possibility of an ERC777 loan asset warrants this as high, I think. Even though they warden didn't suggest that vector
#3 - wilsoncusack
2022-04-08T10:38:11Z
scratch that, I think ERC777 not possible because our contract isn't setup to receive them
#4 - wilsoncusack
2022-04-08T10:50:47Z
er erc777 does work because reception ack is not needed in the normal case
#5 - gzeoneth
2022-04-15T14:40:14Z
Sponsor confirmed
139.9476 USDC - $139.95
Since the borrower is able to specify any asset token, it is possible that loans will be created with tokens that support fee on transfer. If a fee on transfer asset token is chosen, the protocol will contain a point of failure on the original lend()
call.
It is my belief that this is a medium severity vulnerability due to its ability to impact core protocol functionality.
For the first lender to call lend()
, if the transfer fee % of the asset token is larger than the origination fee %, the second transfer will fail in the following code:
ERC20(loanAssetContractAddress).safeTransferFrom(msg.sender, address(this), amount); uint256 facilitatorTake = amount * originationFeeRate / SCALAR; ERC20(loanAssetContractAddress).safeTransfer( IERC721(borrowTicketContract).ownerOf(loanId), amount - facilitatorTake );
Example:
originationFee = 2%
Max fee is 5% per comments
feeOnTransfer = 3%
amount = 100 tokens
Lender transfers amount
NFTLoanFacilitator
receives 97
.
facilitatorTake = 2
NFTLoanFacilitator
attempts to send 100 - 2
to borrower, but only has 97
.
Execution reverts.
If the originationFee is less than or equal to the transferFee, the transfers will succeed but will be received at a loss for the borrower and lender. Specifically for the lender, it might be unwanted functionality for a lender to lend 100 and receive 97 following a successful repayment (excluding interest for this example).
Manual review.
Since the originationFee
is calculated based on the amount
sent by the lender, this calculation will always underflow given the example above. Instead, a potential solution would be to calculate the originationFee
based on the requested loan amount, allowing the lender to send a greater value so that feeOnTransfer <= originationFee
.
Oppositely, the protocol can instead calculate the amount received from the initial transfer and use this amount to calculate the originationFee
. The issue with this option is that the borrower will receive less than the desired loan amount.
#0 - wilsoncusack
2022-04-07T15:39:04Z
#33
#1 - wilsoncusack
2022-04-07T15:44:12Z
If amount - origination_fee - token_fee < 0
then yeah you will not be able to underwrite to loan. But that would be a huge fee
#2 - wilsoncusack
2022-04-07T15:50:27Z
discussed more with 0xDjango, if the token even has a 1% fee then the second transfer will fail OR we will leak facilitator funds, although this is sort of impossible because currently none of the transactions with these fee on transfer tokens will work
#3 - wilsoncusack
2022-04-08T00:08:13Z
this issue is slightly different from others that just point out that borrowers will get amount - token_fee
. the only one I have seen, I think, to point out that fulfilling loans with fee on transfer tokens is impossible
#4 - wilsoncusack
2022-04-08T10:27:08Z
Imagine a token that takes 1% on transfer. Amount = 100 99 reaches facilitator facilitator transfers 100 - facilitator take = 99 to the borrower. Facilitator gets nothing Borrower gets 98.
If the facilitator take is greater or the fee on transfer take is greater, it won't work at all.
#5 - wilsoncusack
2022-04-08T10:27:36Z
Med severity is maybe right given we can miss out on origination fees?
#6 - wilsoncusack
2022-04-13T01:53:50Z
#7 - gzeoneth
2022-04-15T12:43:34Z
Sponsor confirmed with fix
π Selected for report: IllIllI
Also found by: 0v3rf10w, 0x1f8b, 0xDjango, 0xkatana, BouSalman, CertoraInc, Dravee, FSchmoede, Hawkeye, Kenshin, Meta0xNull, PPrieditis, Ruhum, TerrierLover, VAD37, WatchPug, berndartmueller, csanuragjain, hake, horsefacts, hubble, m9800, rayn, reassor, robee, samruna, securerodd, shenwilly, sorrynotsorry, t11s, teryanarmen, tintin, z3s
67.476 USDC - $67.48
Backed has indicated that understanding potentially malicious NFT contracts are the responsibility of the borrower and lender. This submission is aimed to reduce the effect of malicious NFTs on the health of the protocol.
The NFTLoanFacilitator
contract can contain a single check that NFTLoanFacilitator
is the owner of the NFT at the end of the createLoan()
call. In the contract's current state, the borrower can create a loan with a malicious NFT that was never transferred to the NFTLoanFacilitator
contract. A lender would then be responsible to understand that the NFT collateral asset is not able to be seized. Adding the check that NFTLoanFacilitator
is the owner of the NFT will reduce the probability of malicious loans sitting on the protocol.
Loans can be created with a faulty loanAssetContractAddress. This address is correctly checked during the lend function, reverting if it equals the zero address. Might as well move this check to the createLoan()
function to mitigate unwanted faulty loans sitting in the protocol.
Anyone can request a loan with interestRate = 0
and duration as a boundlessly large number. Obviously it is the responsibility of the lender to understand the parameters, but Backed might not desire 0% interest rate loans that basically cannot be collected on due to lengthy terms.
Current max duration is limited by uint32.
maxDuration = 2^32-1 = 4294967295
4294967295 / 60 / 60 / 24 / 365 = 136 years.
Perhaps add maxDuration = 3 years
or similar.
#0 - wilsoncusack
2022-04-08T14:16:41Z
Issue 1 (Low) - Check that NFTLoanFacilitator address is owner of NFT to remove malicious NFT loans
in a malicious contract the ownerOf
call could also lie
will consider max duration