Backed Protocol contest - 0xDjango's results

Protocol for peer to peer NFT-Backed Loans.

General Information

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

Backed Protocol

Findings Distribution

Researcher Performance

Rank: 2/47

Findings: 3

Award: $6,351.94

🌟 Selected for report: 2

πŸš€ Solo Findings: 1

Findings Information

🌟 Selected for report: 0xDjango

Labels

bug
3 (High Risk)
resolved
sponsor confirmed

Awards

6144.5095 USDC - $6,144.51

External Links

Lines of code

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

Vulnerability details

Impact

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.

Proof of Concept

This exploit requires that the loanAssetContractAddress token transfers control to the receiver.

Steps of exploit:

  • Borrower creates loan with createLoan().
  • The same Borrower calls lend(), funding their own loan. The Borrower receives the lend ticket, and funds are transferred to themself.
  • A new lender attempts to buy out the loan. The original loan amount + accruedInterest are sent to the original lender (same person as borrower).
  • Due to lack of checks-effects-interactions pattern, the borrower is able to immediately call 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.

Tools Used

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

Findings Information

🌟 Selected for report: 0xDjango

Also found by: Dravee, IllIllI, Ruhum, cccz, csanuragjain, robee

Labels

bug
2 (Med Risk)
disagree with severity
resolved
sponsor confirmed

Awards

139.9476 USDC - $139.95

External Links

Lines of code

https://github.com/code-423n4/2022-04-backed/blob/e8015d7c4b295af131f017e646ba1b99c8f608f0/contracts/NFTLoanFacilitator.sol#L155-L160

Vulnerability details

Impact

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.

Proof of Concept

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.

Other considerations:

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).

Tools Used

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

Awards

67.476 USDC - $67.48

Labels

bug
QA (Quality Assurance)
sponsor acknowledged

External Links

Issue 1 (Low) - Check that NFTLoanFacilitator address is owner of NFT to remove malicious NFT loans

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.

Issue 2 (Non-critical) - Consider moving zero address check to createLoan()

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.

Issue 3 (Non-critical) - Consider adding max duration

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

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax Β© 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter