Platform: Code4rena
Start Date: 27/04/2022
Pot Size: $50,000 MIM
Total HM: 6
Participants: 59
Period: 5 days
Judge: 0xean
Id: 113
League: ETH
Rank: 7/59
Findings: 3
Award: $1,651.93
🌟 Selected for report: 1
🚀 Solo Findings: 0
https://github.com/code-423n4/2022-04-abranft/blob/main/contracts/NFTPairWithOracle.sol#L287-L288 https://github.com/code-423n4/2022-04-abranft/blob/5cd4edc3298c05748e952f8a8c93e42f930a78c2/contracts/interfaces/INFTOracle.sol#L5-L10
The INFTOracle.get()
function returns two values. A boolean specifying whether a valid rate is available and a uint specifying the current price: https://github.com/code-423n4/2022-04-abranft/blob/5cd4edc3298c05748e952f8a8c93e42f930a78c2/contracts/interfaces/INFTOracle.sol#L5-L10
The NFTPairWithOracle contract only checks the second value and ignores the first one. If the oracle doesn't have a valid price it will return (false, 0)
since the zero-value of the uint will probably be used in a such a case.
If that happens, the lender is able to seize the borrower's collateral. Just because the oracle doesn't have a recent value doesn't mean that the collateral's value dropped. In such a case, the lender shouldn't be able to seize it. Instead, it should simply be ignored until the oracle is able to provide a valid value.
https://github.com/code-423n4/2022-04-abranft/blob/main/contracts/NFTPairWithOracle.sol#L287-L288
uint256 amount = loanParams.valuation + interest; (, uint256 rate) = loanParams.oracle.get(address(this), tokenId); require(rate.mul(loanParams.ltvBPS) / BPS < amount, "NFT is still valued");
If the oracle returns (false, 0)
the require statement will always be true since the left side of the inequality will always be 0 while the right side will always be larger than 0.
none
Change the linked lines to:
uint256 amount = loanParams.valuation + interest; (bool success, uint256 rate) = loanParams.oracle.get(address(this), tokenId); require(success, "oracle doesn't return valid value"); require(rate.mul(loanParams.ltvBPS) / BPS < amount, "NFT is still valued");
#0 - 0xm3rlin
2022-05-04T14:06:22Z
1045.8477 MIM - $1,045.85
https://github.com/code-423n4/2022-04-abranft/blob/main/contracts/NFTPairWithOracle.sol#L198-L223 https://github.com/code-423n4/2022-04-abranft/blob/main/contracts/NFTPairWithOracle.sol#L200-L212 https://github.com/code-423n4/2022-04-abranft/blob/main/contracts/NFTPairWithOracle.sol#L288
The lender should only be able to seize the collateral if:
But, the lender is able to seize the collateral at any time by modifying the loan parameters.
The updateLoanParams()
allows the lender to modify the parameters of an active loan in favor of the borrower. But, by setting the ltvBPS
value to 0
they are able to seize the collateral.
If ltvBPS
is 0
the following require statement in removeCollateral()
will always be true:
https://github.com/code-423n4/2022-04-abranft/blob/main/contracts/NFTPairWithOracle.sol#L288
rate * 0 / BPS < amount
is always true
.
That allows the lender to seize the collateral although its value didn't decrease nor did the time to repay the loan come.
So the required steps are:
updateLoanParams()
to set the ltvBPS
value to 0
removeCollateral()
to steal the collateral from the contractnone
Don't allow updateLoanParams()
to change the ltvBPS
value.
#0 - cryptolyndon
2022-05-05T03:32:42Z
Confirmed, and the first to report this particular exploit.
🌟 Selected for report: IllIllI
Also found by: 0x1337, 0x1f8b, 0xDjango, 0xf15ers, AuditsAreUS, BowTiedWardens, CertoraInc, Funen, GimelSec, MaratCerby, Ruhum, WatchPug, antonttc, berndartmueller, bobi, bobirichman, broccolirob, catchup, cccz, defsec, delfin454000, gs8nrv, gzeon, horsefacts, hubble, hyh, ilan, jah, joestakey, kebabsec, kenta, kenzo, m9800, mics, oyc_109, pauliax, reassor, robee, samruna, sikorico, simon135, throttle, unforgiven, z3s
72.3807 MIM - $72.38
In NFTPair.init()
you verify that the collateral is not a zero-address. But, you don't do the same thing for the asset. If either of them is not a valid address the NFTPair is unusable. So if you do it for the collateral you should also do it for the asset.
Relevant code:
Add:
require(address(asset) != address(0), "NFTPair: invalid asset");
The ERC721 safe methods check whether the receiving address is able to handle ERC721 tokens. In removeCollateral()
you send the collateral to a user defined address. If the user makes a mistake and uses a contract that can't handle the collateral the token will be locked up.
You might argue that it's the user's fault and not the contract's responsibility. But switching to the safe method wouldn't introduce any reentrancy risk. The collateral is already transferred at the very end of the function. After all the relevant state variables are updated.
It provides a little more protection for the user with a small increase in gas costs.
Relevant code:
#0 - cryptolyndon
2022-05-12T04:25:34Z
nit()
is only called once. Having one or both be zero or invalid is otherwise harmless, if wasteful.safeTransferFrom()