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: 10/59
Findings: 3
Award: $1,277.75
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 Selected for report: 0x1337
Also found by: BowTiedWardens, GimelSec, catchup, cccz, gzeon, horsefacts, hyh, kenzo
https://github.com/code-423n4/2022-04-abranft/blob/main/contracts/NFTPairWithOracle.sol#L221 https://github.com/code-423n4/2022-04-abranft/blob/main/contracts/NFTPairWithOracle.sol#L37 https://github.com/code-423n4/2022-04-abranft/blob/main/contracts/NFTPairWithOracle.sol#L205:#L211
When requesting a loan, the borrower sets the used oracle. Afterwards the lender can change the loan parameters as long as they are favorable to the borrower. However, the contract is not checking whether the oracle has been updated. This means the lender can immediately change a loan's oracle to be an oracle of his choosing.
The lender can set the loan's oracle to be an oracle that puts the loan underwater. Therefore the borrower's collateral can be liquidated and taken unjustly.
The TokenLoanParams struct contains the oracle to be used, amongst other fields:
struct TokenLoanParams { uint128 valuation; // How much will you get? OK to owe until expiration. uint64 duration; // Length of loan in seconds uint16 annualInterestBPS; // Variable cost of taking out the loan uint16 ltvBPS; // Required to avoid liquidation INFTOracle oracle; // oracle used }
When the lender updates loan parameters in updateLoanParameters
, the new oracle is not checked:
require( params.duration >= cur.duration && params.valuation <= cur.valuation && params.annualInterestBPS <= cur.annualInterestBPS && params.ltvBPS <= cur.ltvBPS, "NFTPair: worse params" );
The loan parameters are then updated as a whole:
tokenLoanParams[tokenId] = params;
Therefore, the oracle will be also updated, and the lender can choose an oracle that will put the loan underwater and then liquidate the collateral immediately. (Note: also, the log emitted does not log the new oracle.)
Either don't allow the lender to change the oracle, or allow it only if the borrower agrees to the new oracle.
#0 - cryptolyndon
2022-05-05T04:42:58Z
Duplicate of #37, but confirmed.
🌟 Selected for report: kenzo
Also found by: AuditsAreUS, Czar102, GimelSec, WatchPug
542.2914 MIM - $542.29
https://github.com/code-423n4/2022-04-abranft/blob/main/contracts/NFTPair.sol#L218 https://github.com/code-423n4/2022-04-abranft/blob/main/contracts/NFTPairWithOracle.sol#L238
_requestLoan
makes an external call to the collateral contract before updating the NFTPair contract state.
If the ERC721 collateral has a afterTokenTransfer hook, The NFTPair contract can be reentered, and a loan can be requested without the borrower supplying the collateral. Someone can then lend for the loan while the collateral is missing from the contract. Therefore the malicious borrower received the loan without supplying collateral - so lender's funds can be stolen. The issue is present in both NFTPair and NFTPairWithOracle.
Assume the NFT contract has an afterTokenTransfer hook which calls back to the malicious borrower.
POC in short: borrower will call requestLoan
with skim==false
, then during the hook will reenter requestLoan
with skim==true
, then call removeCollateral
to get his collateral back, then the first requestLoan
will resume and initiate the loan, although the collateral is not in the contract any more.
POC in long: the borrower will do the following:
requestLoan
with skim==false.requestLoan
will call collateral.transferFrom()
.afterTokenTransfer
hook, and hand control back to Malbo (malicious borrower).requestLoan
again with skim==true
.requestLoan
(the second) will continue and set the loan details and status.afterTokenTransfer
hook, Malbo will call removeCollateral
. His call will succeed as the loan is in status requested. So the collateral will get sent back to Malbo.afterTokenTransfer
hook finishes.requestLoan
will resume operation at the point where all the loan details will be updated.Move the external call to the end of the function to conform with checks-effects-interaction pattern.
#0 - cryptolyndon
2022-05-05T04:24:20Z
Not a bug. We do not use safeTransfer
, and if the collateral contract cannot be trusted, then all bets are off.
#1 - 0xean
2022-05-20T22:31:54Z
I am downgrading this to medium severity and do believe it should be fixed by the sponsor. Re-entrancy has proved to be a problem in many ways in the space and while the sponsor says they are trusting the collateral contract, I dont think this is a defensible stance from what can be easily mitigated by either re-ordering code to conform to well established patterns or by adding a modifier.
2 — Med: Assets not at direct risk, but the function of the protocol or its availability could be impacted, or leak value with a hypothetical attack path with stated assumptions, but external requirements.
🌟 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
76.5753 MIM - $76.58
The _call
function checks that the external call is not to BentoBox/collateral contracts, as then an attacker could call them and approve himself to spend NFTPair's funds:
require(callee != address(bentoBox) && callee != address(collateral) && callee != address(this), "NFTPair: can't call");
These address variables are being set in the init
function.
If an attacker calls call
before the init
function has been called, he may execute the aforementioned malicious code.
Now, I realize this will probably not happen with AbraNFT, as you are deploying the NFTPair using Bento's deploy
, which initializes the NFTPair atomically. So this can not happen.
However, the risk is there in case somebody forks your project and changes the flow, as we've seen for example in the various Compund hacks and it's tech debts.
So while you are basically not as risk, I suggest you mediate this vector for the potential good of humankind. You can do so by requiring that collateral != 0
etc' or by adding a comment notifying about the risk. May all human beings be happy.
The lend function takes as an argument from the lender the loan params, and checking that they are not worse for the borrower. It then uses the parameters set by the borrower to initialite the loan, and not these parameters from the lender. I think it is a design choice as to which version to use, but just wanted to point this out in case this is by mistake.
#0 - cryptolyndon
2022-05-13T05:31:39Z
The lender parameters are there as a guard against borrowers front running lend()
and changing the terms, not for the lender to update the terms. The expected situation is that the terms passed by the lender are an exact match. However, we felt that if the borrower did change the terms unbeknownst to the lender, but in a way that benefits the lender, then the lender deserves that benefit.
I was going to dismiss the first finding also, but your further justification changed my mind. Spending gas is unfair to callers of the original, hopefully non-botched, version IMO, but a comment is reasonable.