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: 18/59
Findings: 2
Award: $614.93
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: kenzo
Also found by: AuditsAreUS, Czar102, GimelSec, WatchPug
https://github.com/code-423n4/2022-04-abranft/blob/5cd4edc3298c05748e952f8a8c93e42f930a78c2/contracts/NFTPairWithOracle.sol#L294-L297 https://github.com/code-423n4/2022-04-abranft/blob/5cd4edc3298c05748e952f8a8c93e42f930a78c2/contracts/NFTPairWithOracle.sol#L234-L236
There is a potential reentrancy bug they may exist between requestLoan()
and removeCollateral()
that allows a user to have requested a loan while maintaining ownership of the collateral.
This bug is present in both NFTPair
and NFTPairWithOracle
.
The reentrancy attack requires the user to gain control of execution during beforeTokenTransfer()
on the collateral token. While this is rare that developers relinquish control during this step it should be accounted for.
The attack works by first requesting a loan which transfers the collateral
tokenId to the protocol.
The attacker is then able to call removeCollateral()
which will delete the loan then begin to transfer the token. If the attacker is able to gain control during beforeTokenTransfer()
the owner will still be NFTPair
/ NFTPairWithOracle
.
function removeCollateral(uint256 tokenId, address to) public { ... delete tokenLoan[tokenId]; collateral.transferFrom(address(this), to, tokenId); emit LogRemoveCollateral(tokenId, to); }
Since the owner of the contract is still this
it is possible to reenter the contract and requestLoan()
with skim = true
. We will pass the required checks and request a new loan.
function _requestLoan( ... require(tokenLoan[tokenId].status == LOAN_INITIAL, "NFTPair: loan exists"); if (skim) { require(collateral.ownerOf(tokenId) == address(this), "NFTPair: skim failed");
As the reentrancy completes the original removeCollateral()
will finish executing collateral.transferFrom(address(this), to, tokenId);
and the ERC712 token will be transferred to the attacker while the loan will still exist.
There are two mitigations for this issue.
The first is to remove the skim
functionality and instead forces the contract to transfer the token from the user to the NFTPair
/ NFTPairWithOracle
.
The second mitigation is to add a reentrancy guard to each of the following functions:
requestLoan()
removeCollateral()
requestAndBorrow()
takeCollateralAndLend()
repay()
lend()
#0 - cryptolyndon
2022-05-06T04:14:56Z
Duplicate of #61
🌟 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.6404 MIM - $72.64
https://github.com/code-423n4/2022-04-abranft/blob/5cd4edc3298c05748e952f8a8c93e42f930a78c2/contracts/NFTPairWithOracle.sol#L192-L196 https://github.com/code-423n4/2022-04-abranft/blob/5cd4edc3298c05748e952f8a8c93e42f930a78c2/contracts/NFTPair.sol#L175-L179
The function init()
in both NFTPair
and NFTPairWithOracle
are payable
but do not use msg.value
.
The impact is that any native currency sent in the init()
function will sit in the contract. These tokens may be claimed by the first user to call cook()
with ACTION_CALL
to transfer the value to an attacker controlled address.
function init(bytes calldata data) public payable override { require(address(collateral) == address(0), "NFTPair: already initialized"); (collateral, asset) = abi.decode(data, (IERC721, IERC20)); require(address(collateral) != address(0), "NFTPair: bad pair"); }
Consider enforcing msg.value == 0
in the init()
function.
#0 - 0xm3rlin
2022-05-04T14:13:21Z
There is no specific risk except of the initiator to loose some ETH. this is not a function to be expected to be called by non proficient users
#1 - 0xean
2022-05-21T00:25:46Z
@0xm3rlin - is the payable
modifier here just for gas savings?
#2 - 0xean
2022-05-21T14:54:11Z
Downgrading to QA.
#3 - JeeberC4
2022-05-23T19:08:53Z
Preserving original title: init() Is Payable But msg.value Is Not Used