Platform: Code4rena
Start Date: 28/11/2022
Pot Size: $192,500 USDC
Total HM: 33
Participants: 106
Period: 11 days
Judge: LSDan
Total Solo HM: 15
Id: 186
League: ETH
Rank: 12/106
Findings: 6
Award: $2,254.07
π Selected for report: 2
π Solo Findings: 0
685.9021 USDC - $685.90
Punk NFT was developed many years back before ERC721 standard was established. Thus, they took inspiration from the ERC20 standard and removed the transfer
and transferFrom
functions to achieve the "Non-Fungible" characteristic of their NFT "token".
Back then, there was also no approve
or setApprovalForAll
function since ERC721 standard had not been established yet. Thus, there is no way to grant allowance to the marketplace to spend or manage the punk NFTs on behalf of the users. As such, a workaround is needed, and the workaround is to use Punk.offerPunkForSaleToAddress
, Punk.offerPunkForSale,
and Punk.buyPunk
to achieve the same result as allowance/approval in ERC721.
How it works is that the seller will call offerPunkForSaleToAddress
to grant allowance to the marketplace/operator, and the marketplace/operator will call buyPunk
to transfer the NFT from the seller.
This workaround is used within the WPunkGateway.acceptBidWithCredit
function.
Following is the specification of the CryptoPunksMarket.offerPunkForSaleToAddress
function.
File: CryptoPunksMarket.sol 210: function offerPunkForSaleToAddress( 211: uint256 punkIndex, 212: uint256 minSalePriceInWei, 213: address toAddress 214: ) public {
The following gives an overview of how the WPunkGateway.acceptBidWithCredit
function works under the hood. This is needed to understand the vulnerability.
Bob (maker) bids Alice's Punk NFT #2 for 1000 ETH. The 1000 ETH consists of 800 ETH payNowAmount and 200 ETH credit. In other words, Bob has to pay 800 ETH from his wallet and borrow 200 ETH from ParaSpace when the deal happens.
Suppose Alice (taker) wants to accept Bob's bid for her Punk NFT #2 (punkIndex=2). In that case, Alice will call the CryptoPunksMarket.offerPunkForSaleToAddress(2, 0, Punk_Gateway_Address)
first.
Alice will call the WPunkGateway.acceptBidWithCredit
function. The Punk Gateway will obtain/buy Alice's Punk NFT #2 from her wallet. Since the offer.minValue
is zero, the Punk Gateway does not need to send any ETH for the transaction. Note that only the Punk Gateway can buy Alice's Punk NFT #2 because the offer.onlySellTo
has been explicitly set to the Punk Gateway. Thus, there is no risk that other users can obtain Alice's Punk NFT #2 for zero wei.
The Punk Gateway will send the newly obtained Punk NFT #2 to WPunk Proxy to mint the Wrapped Punk NFT #2. Newly minted wrapped Punk NFT #2 will be returned to the Punk Gateway.
The Punk Gateway will send the wrapped Punk NFT #2 back to Alice's wallet.
The Pool.acceptBidWithCredit
function in Line 148 below will be triggered, and the orders will be executed. Alice will receive 1000 ETH, and her Punk NFT #2 will the sent to ParaSpace.
ParaSpace will collateralize the Punk NFT #2 on Bob's behalf and mint respective nWPunk token to Bob that represents his claim to the Punk NFT #2 in ParaSpace.
File: WPunkGateway.sol 129: function acceptBidWithCredit( 130: bytes32 marketplaceId, 131: bytes calldata payload, 132: DataTypes.Credit calldata credit, 133: uint256[] calldata punkIndexes, 134: uint16 referralCode 135: ) external nonReentrant { 136: for (uint256 i = 0; i < punkIndexes.length; i++) { 137: Punk.buyPunk(punkIndexes[i]); 138: Punk.transferPunk(proxy, punkIndexes[i]); 139: // gatewayProxy is the sender of this function, not the original gateway 140: WPunk.mint(punkIndexes[i]); 141: 142: IERC721(wpunk).safeTransferFrom( 143: address(this), 144: msg.sender, 145: punkIndexes[i] 146: ); 147: } 148: Pool.acceptBidWithCredit( 149: marketplaceId, 150: payload, 151: credit, 152: msg.sender, 153: referralCode 154: ); 155: }
However, there is a flaw in the WPunkGateway.acceptBidWithCredit
function that allows anyone to steal Taker's Punk NFTs.
Assume that Alice calls the CryptoPunksMarket.offerPunkForSaleToAddress(2, 0, Punk_Gateway_Address)
to "grant" the Punk Gateway the ability to obtain her Punk NFT #2 as described in the overview of the function above.
Next, Alice calls the WPunkGateway.acceptBidWithCredit
function to accept the bid.
At this point, the sequence of execution in the mempool is as follows (First one will be executed first):
CryptoPunksMarket.offerPunkForSaleToAddress(2, 0, Punk_Gateway_Address)
- Caller = AliceWPunkGateway.acceptBidWithCredit(...punkIndexes=2...)
- Caller AliceMallory (attacker) saw these transactions in the mempool and front-run Alice's second transaction. He calls the WPunkGateway.acceptBidWithCredit
with the punkIndexes
point to Punk NFT #2, and arbitrary bogus/non-relevant accept bid payloads (e.g. trading among his accounts) that does not cause a revert.
After the front-running attack, the sequence of execution in the mempool is as follows (First one will be executed first):
CryptoPunksMarket.offerPunkForSaleToAddress(2, 0, Punk_Gateway_Address)
- Caller = AliceWPunkGateway.acceptBidWithCredit(...punkIndexes=2...)
- Caller Mallory (Front-Run)WPunkGateway.acceptBidWithCredit(...punkIndexes=2...)
- Caller AliceNote that Line 142 of the WPunkGateway.acceptBidWithCredit
will send the newly minted Wrapped Punk NFT #2 to the msg.sender
. In this case, Mallory's WPunkGateway.acceptBidWithCredit
transaction will be executed before Alice's one. Therefore, the caller is Mallory, and the Wrapped Punk NFT #2 will be sent to Mallory's wallet.
Alice's WPunkGateway.acceptBidWithCredit
will subsequently revert.
Users' Punk NFTs will be stolen
Update the WPunkGateway.acceptBidWithCredit
to ensure that the newly minted wrapped Punk NFT is not blindly sent to the caller (msg.sender) because the caller might not be the owner/seller of the Punk NFT.
Instead, the newly minted wrapped Punk NFT should be sent back to the original owner/seller of the Punk NFT.
function acceptBidWithCredit( bytes32 marketplaceId, bytes calldata payload, DataTypes.Credit calldata credit, uint256[] calldata punkIndexes, uint16 referralCode ) external nonReentrant { for (uint256 i = 0; i < punkIndexes.length; i++) { + address originalOwner = Punk.punkIndexToAddress[punkIndexes[i]]; // Find the owner of the Punk NFT + Punk.buyPunk(punkIndexes[i]); Punk.transferPunk(proxy, punkIndexes[i]); // gatewayProxy is the sender of this function, not the original gateway WPunk.mint(punkIndexes[i]); IERC721(wpunk).safeTransferFrom( address(this), - msg.sender, + originalOwner punkIndexes[i] ); } Pool.acceptBidWithCredit( marketplaceId, payload, credit, msg.sender, referralCode ); }
#0 - JeffCX
2022-12-18T04:21:10Z
duplicate of https://github.com/code-423n4/2022-11-paraspace-findings/issues/137 because of the sponsor confirmed tag.
#1 - c4-judge
2022-12-20T18:11:26Z
dmvt marked the issue as duplicate of #71
#2 - c4-judge
2023-01-23T20:01:41Z
dmvt marked the issue as satisfactory
π Selected for report: IllIllI
Also found by: 0xNazgul, Atarpara, Awesome, Aymen0909, BClabs, Kong, ali_shehab, bullseye, chaduke, csanuragjain, datapunk, fatherOfBlocks, hansfriese, kaliberpoziomka8552, nicobevi, pashov, pzeus, shark, unforgiven, web3er, xiaoming90
44.934 USDC - $44.93
The NFTFloorOracle.removeFeeder
function allows the caller to remove a feeder from the NFTFloorOracle. However, this function lacks of access control, and thus anyone can call this function to remove all feeders from the NFTFloorOracle.
File: NFTFloorOracle.sol 165: /// @notice Allows owner to remove feeder. 166: /// @param _feeder feeder to remove 167: function removeFeeder(address _feeder) 168: external 169: onlyWhenFeederExisted(_feeder) 170: { 171: _removeFeeder(_feeder); 172: }
The onlyWhenFeederExisted
modifier only checks if the feeder exists. It does not check if the caller is the owner.
File: NFTFloorOracle.sol 127: modifier onlyWhenFeederExisted(address _feeder) { 128: require(_isFeederExisted(_feeder), "NFTOracle: feeder not existed"); 129: _; 130: }
File: NFTFloorOracle.sol 274: function _isFeederExisted(address _feeder) internal view returns (bool) { 275: return feederPositionMap[_feeder].registered; 276: }
Similarly, the _removeFeeder
internal function only checks if the feeder exists. None of the code check if the caller is the owner.
File: NFTFloorOracle.sol 326: function _removeFeeder(address _feeder) 327: internal 328: onlyWhenFeederExisted(_feeder) 329: { 330: uint8 feederIndex = feederPositionMap[_feeder].index; 331: if (feederIndex >= 0 && feeders[feederIndex] == _feeder) { 332: feeders[feederIndex] = feeders[feeders.length - 1]; 333: feeders.pop(); 334: } 335: delete feederPositionMap[_feeder]; 336: revokeRole(UPDATER_ROLE, _feeder); 337: emit FeederRemoved(_feeder); 338: }
Thus, anyone can call NFTFloorOracle.removeFeeder
function.
Anyone can remove all feeders from the Oracle. Thus, no one will be able to feed the latest prices to NFTFloorOracle. The entire protocol relies on NFTFloorOracle to function properly. Without the latest price, the market value of NFT cannot be determined, and the collateral will end up being overvalued or undervalued. This causes assets to be liquidated prematurely OR allow users to overborrow.
Additionally, if there is a malicious feeder, the malicious feeder can remove the other feeders and gain full control of the price within ParaSpace.
Ensure that only owner can remove the feeder from the NFTFloorOracle.
/// @notice Allows owner to remove feeder. /// @param _feeder feeder to remove function removeFeeder(address _feeder) external - onlyWhenFeederExisted(_feeder) + onlyRole(DEFAULT_ADMIN_ROLE) { _removeFeeder(_feeder); }
#0 - JeffCX
2022-12-18T04:02:58Z
#1 - c4-judge
2022-12-20T16:59:00Z
dmvt marked the issue as duplicate of #31
#2 - c4-judge
2023-01-23T16:00:25Z
dmvt marked the issue as satisfactory
#3 - c4-judge
2023-01-23T16:06:39Z
dmvt marked the issue as partial-50
#4 - c4-judge
2023-01-23T16:06:54Z
dmvt marked the issue as full credit
#5 - c4-judge
2023-01-23T20:42:00Z
dmvt marked the issue as satisfactory
π Selected for report: xiaoming90
Also found by: csanuragjain, unforgiven
951.3351 USDC - $951.34
This section provides a context of the pre-requisite concepts that a reader needs to fully understand the issue.
Assume that Jimmy is the owner of BAYC #8888 and BAKC #9999 NFTs initially. He participated in the Paired Pool and staked + accrued a total of 100 ApeCoin (APE) at this point, as shown in the diagram below.
Jimmy then sold his BAKC #9999 NFT to Ben. When this happens, both parties (Jimmy and Ben) could close out their staking position. Since Ben owns BAKC #9999 now, he can close out Jimmy's position anytime and claim all the accrued APE rewards (2 APE below). While Jimmy will obtain the 98 APE that he staked initially.
The following image is taken from https://youtu.be/_LO-1f9nyjs?t=640
The ApeCoinStaking._withdrawPairNft
taken from the official $APE Staking Contract shows that the implementation allows both the BAYC/MAYC owners and BAKC owners to close out the staking position. Refer to Line 976 below.
When the staking position is closed by the BAKC owners, the entire staking amount must be withdrawn. A partial amount is not allowed per Line 981 below. In Line 984, all the accrued APE rewards will be sent to the BAKC owners. In Line 989, all the staked APEs will be withdrawn (unstake) and sent directly to the wallet of the BAYC owners.
File: ApeCoinStaking.sol 966: function _withdrawPairNft(uint256 mainTypePoolId, PairNftWithAmount[] calldata _nfts) private { 967: for(uint256 i; i < _nfts.length; ++i) { 968: uint256 mainTokenId = _nfts[i].mainTokenId; 969: uint256 bakcTokenId = _nfts[i].bakcTokenId; 970: uint256 amount = _nfts[i].amount; 971: address mainTokenOwner = nftContracts[mainTypePoolId].ownerOf(mainTokenId); 972: address bakcOwner = nftContracts[BAKC_POOL_ID].ownerOf(bakcTokenId); 973: PairingStatus memory mainToSecond = mainToBakc[mainTypePoolId][mainTokenId]; 974: PairingStatus memory secondToMain = bakcToMain[bakcTokenId][mainTypePoolId]; 975: 976: require(mainTokenOwner == msg.sender || bakcOwner == msg.sender, "At least one token in pair must be owned by caller"); 977: require(mainToSecond.tokenId == bakcTokenId && mainToSecond.isPaired 978: && secondToMain.tokenId == mainTokenId && secondToMain.isPaired, "The provided Token IDs are not paired"); 979: 980: Position storage position = nftPosition[BAKC_POOL_ID][bakcTokenId]; 981: require(mainTokenOwner == bakcOwner || amount == position.stakedAmount, "Split pair can't partially withdraw"); 982: 983: if (amount == position.stakedAmount) { 984: uint256 rewardsToBeClaimed = _claim(BAKC_POOL_ID, position, bakcOwner); 985: mainToBakc[mainTypePoolId][mainTokenId] = PairingStatus(0, false); 986: bakcToMain[bakcTokenId][mainTypePoolId] = PairingStatus(0, false); 987: emit ClaimRewardsPairNft(msg.sender, rewardsToBeClaimed, mainTypePoolId, mainTokenId, bakcTokenId); 988: } 989: _withdraw(BAKC_POOL_ID, position, amount, mainTokenOwner); 990: emit WithdrawPairNft(msg.sender, amount, mainTypePoolId, mainTokenId, bakcTokenId); 991: } 992: }
The following shows that the ApeCoinStaking._withdraw
used in the above function will send the unstaked APEs directly to the wallet of BAYC owners. Refer to Line 946 below.
File: ApeCoinStaking.sol 937: function _withdraw(uint256 _poolId, Position storage _position, uint256 _amount, address _recipient) private { 938: require(_amount <= _position.stakedAmount, "Can't withdraw more than staked amount"); 939: 940: Pool storage pool = pools[_poolId]; 941: 942: _position.stakedAmount -= _amount; 943: pool.stakedAmount -= _amount; 944: _position.rewardsDebt -= (_amount * pool.accumulatedRewardsPerShare).toInt256(); 945: 946: apeCoin.safeTransfer(_recipient, _amount); 947: }
The BAYC is held by the NTokenBAYC
reserve pool, while the MAYC is held by NTokenMAYC
reserve pool. This causes an issue because, as mentioned in the previous section, all the unstaked APE will be sent directly to the wallet of the BAYC/MAYC owners. This will be used as part of the attack path later on.
No. For the purpose of APE staking via ParaSpace , BAKC NFT need not be held in the ParaSpace contract for staking, but Bored Apes and Mutant Apes must be collateralized in the ParaSpace protocol. Refer to here. As such, users are free to sell off their BAKC anytime to anyone since it is not being locked up.
Using back the same example in the previous section. Assume the following:
Ben will close out Jimmy's position by calling the ApeCoinStaking.withdrawBAKC
function of the official $APE Staking Contract to withdraw all the staked APE + accrued APE rewards. As a result, the 98 APE that Jimmy staked initially will be sent directly to the owner of the BAYC #8888 owner. In this case, the BAYC #8888 owner is ParaSpace's NTokenBAYC
reserve pool.
At this point, it is important to note that Jimmy's 98 APE is stuck in ParaSpace's NTokenBAYC
reserve pool. If anyone can siphon out Jimmy's 98 APE that is stuck in the contract, that person will be able to get free APE.
There exist a way to siphon out all the APE coin that resides in ParaSpace's NTokenBAYC
reserve pool. Anyone who participates in APE staking via ParaSpace with a BAYC, which also means that any user staked in the Paired Pool, can trigger the ApeStakingLogic.withdrawBAKC
function below by calling the PoolApeStaking.withdrawBAKC
function.
Notice that in Line 53 below, it will compute the total balance of APE held by ParaSpace's NTokenBAYC
reserve pool contract. In Line 55, it will send all the APE in the pool contract to the recipient. This effectively performs a sweeping of APE stored in the pool contract.
File: ApeStakingLogic.sol 38: function withdrawBAKC( 39: ApeCoinStaking _apeCoinStaking, 40: uint256 poolId, 41: ApeCoinStaking.PairNftWithAmount[] memory _nftPairs, 42: address _apeRecipient 43: ) external { 44: ApeCoinStaking.PairNftWithAmount[] 45: memory _otherPairs = new ApeCoinStaking.PairNftWithAmount[](0); 46: 47: if (poolId == BAYC_POOL_ID) { 48: _apeCoinStaking.withdrawBAKC(_nftPairs, _otherPairs); 49: } else { 50: _apeCoinStaking.withdrawBAKC(_otherPairs, _nftPairs); 51: } 52: 53: uint256 balance = _apeCoinStaking.apeCoin().balanceOf(address(this)); 54: 55: _apeCoinStaking.apeCoin().safeTransfer(_apeRecipient, balance); 56: }
Thus, after Ben closes out Jimmy's position by calling the ApeCoinStaking.withdrawBAKC
function and causes the 98 APE to be sent to ParaSpace's NTokenBAYC
reserve pool contract, Ben immediately calls the PoolApeStaking.withdrawBAKC
function against his own BAYC #0002 and BAKC #0002 NFTs. This will result in all of Jimmy's 98 APE being swept to his wallet. Bob effectively steals Jimmy's 98 APE.
ApeCoin of ParaSpace users can be stolen.
Consider the potential side effects of the split pair edge case in the pair pool when implementing the APE staking feature in ParaSpace.
The official APE staking contract has been implemented recently, and only a few protocols have integrated with it. Thus, the edge cases are not widely understood and are prone to errors.
To mitigate this issue, instead of returning BAKC NFT to the users after staking, consider locking up BAKC NFT in the ParaSpace contract as part of the user's collateral. In this case, the user will not be able to sell off their BAKC, and the "Split Pair Edge Case In Paired Pool" scenario will not happen.
#0 - c4-judge
2022-12-20T15:19:19Z
dmvt marked the issue as duplicate of #138
#1 - c4-judge
2022-12-20T15:27:05Z
dmvt marked the issue as selected for report
#2 - c4-judge
2023-01-23T20:05:23Z
dmvt marked the issue as satisfactory
#3 - dmvt
2023-01-27T10:39:24Z
I've decided to downgrade this to medium. I think the risks involved are understood by most educated users of the BAYC/MAYC universe, but still valid.
#4 - c4-judge
2023-01-27T10:39:32Z
dmvt changed the severity to 2 (Med Risk)
#5 - C4-Staff
2023-02-01T19:10:24Z
captainmangoC4 marked the issue as selected for report
π Selected for report: xiaoming90
Also found by: cccz, csanuragjain, imare, unforgiven
462.3488 USDC - $462.35
The NTokenMoonBirds Reserve Pool only allows Moonbird NFT to be sent to the pool contract as shown in Line 70 below.
File: NTokenMoonBirds.sol 63: function onERC721Received( 64: address operator, 65: address from, 66: uint256 id, 67: bytes memory 68: ) external virtual override returns (bytes4) { 69: // only accept MoonBird tokens 70: require(msg.sender == _underlyingAsset, Errors.OPERATION_NOT_SUPPORTED); 71: 72: // if the operator is the pool, this means that the pool is transferring the token to this contract 73: // which can happen during a normal supplyERC721 pool tx 74: if (operator == address(POOL)) { 75: return this.onERC721Received.selector; 76: } 77: 78: // supply the received token to the pool and set it as collateral 79: DataTypes.ERC721SupplyParams[] 80: memory tokenData = new DataTypes.ERC721SupplyParams[](1); 81: 82: tokenData[0] = DataTypes.ERC721SupplyParams({ 83: tokenId: id, 84: useAsCollateral: true 85: }); 86: 87: POOL.supplyERC721FromNToken(_underlyingAsset, tokenData, from); 88: 89: return this.onERC721Received.selector; 90: }
Note that the NTokenMoonBirds Reserve Pool is the holder/owner of all Moonbird NFTs within ParaSpace. If there is any airdrop for Moonbird NFT, the airdrop will be sent to the NTokenMoonBirds Reserve Pool.
However, due to the validation in Line 70, the NTokenMoonBirds Reserve Pool will not be able to receive any airdrop (e.g. Moonbirds Oddities NFT) other than the Moonbird NFT. In summary, if any NFT other than Moonbird NFT is sent to the pool, it will revert.
For instance, Moonbirds Oddities NFT has been airdropped to Moonbird NFT nested stakers in the past. With the nesting staking feature, more airdrops are expected in the future. In this case, any users who collateralized their nested Moonbird NFT within ParaSpacwill lose their opportunities to claim the airdrop.
Loss of assets for the user. Users will not be able to receive any airdropped assets for their nested Moonbird NFT.
Update the contract to allow airdrops to be received by the NTokenMoonBirds Reserve Pool.
function onERC721Received( address operator, address from, uint256 id, bytes memory ) external virtual override returns (bytes4) { - // only accept MoonBird tokens - require(msg.sender == _underlyingAsset, Errors.OPERATION_NOT_SUPPORTED); // if the operator is the pool, this means that the pool is transferring the token to this contract // which can happen during a normal supplyERC721 pool tx if (operator == address(POOL)) { return this.onERC721Received.selector; } + if msg.sender == _underlyingAsset( // supply the received token to the pool and set it as collateral DataTypes.ERC721SupplyParams[] memory tokenData = new DataTypes.ERC721SupplyParams[](1); tokenData[0] = DataTypes.ERC721SupplyParams({ tokenId: id, useAsCollateral: true }); + ) POOL.supplyERC721FromNToken(_underlyingAsset, tokenData, from); return this.onERC721Received.selector; }
#0 - c4-judge
2022-12-20T15:38:28Z
dmvt marked the issue as duplicate of #164
#1 - c4-judge
2023-01-09T13:32:24Z
dmvt changed the severity to 2 (Med Risk)
#2 - c4-judge
2023-01-23T20:10:55Z
dmvt marked the issue as selected for report
#3 - C4-Staff
2023-02-01T19:10:34Z
captainmangoC4 marked the issue as selected for report
π Selected for report: IllIllI
Also found by: 0x4non, 0x52, 0xAgro, 0xNazgul, 0xSmartContract, 0xackermann, 9svR6w, Awesome, Aymen0909, B2, BRONZEDISC, Bnke0x0, Deekshith99, Deivitto, Diana, Dravee, HE1M, Jeiwan, Kaiziron, KingNFT, Lambda, Mukund, PaludoX0, RaymondFam, Rolezn, Sathish9098, Secureverse, SmartSek, __141345__, ahmedov, ayeslick, brgltd, cccz, ch0bu, chrisdior4, cryptonue, cryptostellar5, csanuragjain, datapunk, delfin454000, erictee, gz627, gzeon, helios, i_got_hacked, ignacio, imare, jadezti, jayphbee, joestakey, kankodu, ksk2345, ladboy233, martin, nadin, nicobevi, oyc_109, pashov, pavankv, pedr02b2, pzeus, rbserver, ronnyx2017, rvierdiiev, shark, unforgiven, xiaoming90, yjrwkk
103.9175 USDC - $103.92
When a Moonbird NFT is nested, the transfer function is disabled. Thus, the usual ERC721's transfer
, transferFrom
and safeTransferFrom
function will not work for a Nested Moonbird NFT.
The following is extracted directly from ParaSpace's Audit Technical documentation.
Specifically, MoonBirds has a nesting functionality which disables the the function safeTransferFrom . additionally, it has a nesting toggle and nesting information that holders would like to have access to after supplying the token to Paraspace. In order to support the transferring of the token while nesting the βowner (not approved operators)β of the token can call a custom function called safeTransferWhileNesting
The flash claim feature relies on the ERC721's transfer
, transferFrom
, and safeTransferFrom
functions. Thus, if the users attempt to perform a flash claim against his Nested MoonBird NFT, the function will revert.
The flash claim feature relies on the FlashClaimLogic.executeFlashClaim
function. Within the FlashClaimLogic.executeFlashClaim
function, it relies on ERC721's safeTransferFrom
function to move underlying NFT forward to and backward from receiver contract. Since Nested MoonBird NFT disable the usual ERC721's transfer functionaility, the function will revert.
File: FlashClaimLogic.sol 28: function executeFlashClaim( 29: DataTypes.PoolStorage storage ps, 30: DataTypes.ExecuteFlashClaimParams memory params 31: ) external { 32: DataTypes.ReserveData storage reserve = ps._reserves[params.nftAsset]; 33: address nTokenAddress = reserve.xTokenAddress; 34: ValidationLogic.validateFlashClaim(ps, params); 35: 36: uint256 i; 37: // step 1: moving underlying asset forward to receiver contract 38: for (i = 0; i < params.nftTokenIds.length; i++) { 39: INToken(nTokenAddress).transferUnderlyingTo( 40: params.receiverAddress, 41: params.nftTokenIds[i] 42: ); 43: } 44: 45: // step 2: execute receiver contract, doing something like airdrop 46: require( 47: IFlashClaimReceiver(params.receiverAddress).executeOperation( 48: params.nftAsset, 49: params.nftTokenIds, 50: params.params 51: ), 52: Errors.INVALID_FLASH_CLAIM_RECEIVER 53: ); 54: 55: // step 3: moving underlying asset backward from receiver contract 56: for (i = 0; i < params.nftTokenIds.length; i++) { 57: IERC721(params.nftAsset).safeTransferFrom( 58: params.receiverAddress, 59: nTokenAddress, 60: params.nftTokenIds[i] 61: ); 62: 63: emit FlashClaim( 64: params.receiverAddress, 65: msg.sender, 66: params.nftAsset, 67: params.nftTokenIds[i] 68: ); 69: } ..SNIP.. 90: }
Users will not be able to use the flash-claim feature to claim their nested MoonBird NFT. If the users need to flash-claim their nested MoonBird NFT to claim any rewards or airdrop, they will not be able to do so.
Considering implement a seperate flash-claim feature for nested MoonBird NFT that depends on safeTransferWhileNesting
function instead of safeTransferFrom
function
#0 - c4-judge
2023-01-09T22:38:16Z
dmvt changed the severity to QA (Quality Assurance)
#1 - c4-judge
2023-01-25T16:16:16Z
dmvt marked the issue as grade-b