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: 23/47
Findings: 2
Award: $164.03
π Selected for report: 0
π Solo Findings: 0
π 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
88.6662 USDC - $88.67
https://github.com/code-423n4/2022-04-backed/blob/main/contracts/NFTLoanFacilitator.sol#L88 https://github.com/code-423n4/2022-04-backed/blob/main/contracts/NFTLoanFacilitator.sol#L262-L266
Lender cannot seize the collateral as expected which ends up simply providing token to the malicious NFT holder.
The issue could happen in this scenario:
(1) An attacker prepares a malicious NFT whose transferFrom and safeTransferFrom functions do nothing:
contract MaliciousERC721 is ERC721 { constructor() ERC721("Malicious", "MALICIOUS") {} ..... function transferFrom( address from, address to, uint256 tokenId ) public virtual override { // Do nothing } function safeTransferFrom( address from, address to, uint256 tokenId ) public virtual override { // Do nothing }
(2) The attacker provides the malicious NFT using createLoan function in NFTLoanFacilitator.sol
Specifically, the attacker sets the malicious NFT contract at collateralContractAddress variable at createLoan function. transferFrom is called at in createLoan function, but transferFrom does not transfer the NFT correctly to the NFTLoanFacilitator contract.
IERC721(collateralContractAddress).transferFrom(msg.sender, address(this), collateralTokenId);
https://github.com/code-423n4/2022-04-backed/blob/main/contracts/NFTLoanFacilitator.sol#L88
(3) A victim lends the token to the attacker using lend function in NFTLoanFacilitator.sol without noticing the issue of malicious NFT. The attacker can receive the token without any issues.
(4) The attacker can just do nothing, and they do not have any worries of being their malicious NFT taken.
Static code analysis
In addition, I checked that the above mentioned scenario can happen using test script.
As a temporary fix, the contract should ensure that the transfer truely happened using balanceOf function of the NFT. But even though this check exists, there are various ways that the attacker can take such as followings:
Hence, it may be worth considering the whitelists of NFTs which can be used at createLoan function.
If lending money to the malicious NFT is all users' faults, this may not be a high pri issue.
#0 - wilsoncusack
2022-04-07T12:23:38Z
won't fix, cannot stop every attack from bad erc20s/nfts
#1 - gzeoneth
2022-04-15T11:51:43Z
Agree with sponsor, downgrading to Low/QA. Treating as warden's QA report.
#2 - JeeberC4
2022-04-17T04:54:35Z
Preserving original title: Malicious NFT (ERC721) can be set at createLoan function at NFTLoanFacilitator.sol
π Selected for report: Dravee
Also found by: 0v3rf10w, 0x1f8b, 0xkatana, CertoraInc, FSchmoede, Funen, IllIllI, Kenshin, Meta0xNull, TerrierLover, Tomio, csanuragjain, joshie, obront, rayn, rfa, robee, saian, securerodd, sorrynotsorry, t11s, z3s
75.3596 USDC - $75.36
In these contracts, it uses require function with string message. https://github.com/code-423n4/2022-04-backed/blob/d34ddbdaf8d1bc1bf17446df830db629ee551308/contracts/BorrowTicket.sol https://github.com/code-423n4/2022-04-backed/blob/d34ddbdaf8d1bc1bf17446df830db629ee551308/contracts/LendTicket.sol https://github.com/code-423n4/2022-04-backed/blob/d34ddbdaf8d1bc1bf17446df830db629ee551308/contracts/NFTLoanFacilitator.sol https://github.com/code-423n4/2022-04-backed/blob/d34ddbdaf8d1bc1bf17446df830db629ee551308/contracts/NFTLoanTicket.sol
Using custom errors can reduce the gas deployment cost so the team can consider using it.
Use custom errors instead of require function with string message.
This is an example of just using the pattern of custom errors and revert at notClosed modifier in NFTLoanFacilitator.sol, but it can reduce deployment gas cost and contract size.
error LoanClosed(); // ==== modifiers ==== modifier notClosed(uint256 loanId) { if (loanInfo[loanId].closed) { revert LoanClosed(); } _; }
Contract | Before | After | Change |
---|---|---|---|
NFTLoanFacilitator | 3203701 | 3189441 | -14260 |
Contract | Before | After | Change(KB) |
---|---|---|---|
NFTLoanFacilitator | 24.018 | 23.987 | -0.066 |
Just using the pattern of custom errors and revert at notClosed modifier in NFTLoanFacilitator.sol, but the above mentioned improvements can be seen. If the all of the codebase uses this patten, it would reduce lots of gas deployment cost and contract size.
https://github.com/code-423n4/2022-04-backed/blob/d34ddbdaf8d1bc1bf17446df830db629ee551308/contracts/NFTLoanFacilitator.sol#L198 https://github.com/code-423n4/2022-04-backed/blob/d34ddbdaf8d1bc1bf17446df830db629ee551308/contracts/NFTLoanFacilitator.sol#L321
Use use != 0 instead of > 0.
if (amountIncrease != 0) {
require(_improvementRate != 0, 'NFTLoanFacilitator: 0 improvement rate');
Confirmed that the deployment gas cost of NFTLoanFacilitator.sol slightly reduced
Sinve INTEREST_RATE_DECIMALS is a fixed value, it is clear that the calculation will not overflow.
unchecked { require(_originationFeeRate <= 5 * (10 ** (INTEREST_RATE_DECIMALS - 2)), "NFTLoanFacilitator: max fee 5%"); }
Confirmed that the gas cost is reduced for NFTLoanFacilitator.sol.
Contract | Before | After | Change |
---|---|---|---|
PaladinRewardReserve | 752599 | 749055 | -3544 |
Contract | Before | After | Change(KB) |
---|---|---|---|
HolyPaladinToken | 24.018 | 23.987 | -0.031 |
uint256 facilitatorTake = amount * originationFeeRate / SCALAR; ERC20(loanAssetContractAddress).safeTransfer( IERC721(borrowTicketContract).ownerOf(loanId), amount - facilitatorTake );
facilitatorTake is only used once, so this is no need to be defined.
ERC20(loanAssetContractAddress).safeTransfer( IERC721(borrowTicketContract).ownerOf(loanId), amount - amount * originationFeeRate / SCALAR );
Both method and deployment gas cost decreased.
Slight contract size reduction is confirmed.
#0 - wilsoncusack
2022-04-07T12:20:02Z
Using unchecked at updateOriginationFeeRate.sol can reduce the gas cost and
ack, this is new and legit
facilitatorTake variable is not needed in NFTLoanFacilitator.sol
yes but it's like 6 gas and improves readability