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: 6/106
Findings: 9
Award: $5,428.52
🌟 Selected for report: 2
🚀 Solo Findings: 0
🌟 Selected for report: Englave
Also found by: 9svR6w, Jeiwan, Josiah, Lambda, RaymondFam, Trust, csanuragjain, kaliberpoziomka8552, minhquanym, unforgiven
286.3766 USDC - $286.38
While removing any feeder the index is not updated. This incorrection in indexing will lead to _removeFeeder
fail everytime as shown in POC. This could lead to problem when owner wants to remove a malicious feeder and would be unable to.
feeders = {A,B} feederPositionMap[A].index = 0 feederPositionMap[B].index = 1
removeFeeder
functionfunction removeFeeder(address _feeder) external onlyWhenFeederExisted(_feeder) { _removeFeeder(_feeder); }
_removeFeeder
functionfunction _removeFeeder(address _feeder) internal onlyWhenFeederExisted(_feeder) { uint8 feederIndex = feederPositionMap[_feeder].index; if (feederIndex >= 0 && feeders[feederIndex] == _feeder) { feeders[feederIndex] = feeders[feeders.length - 1]; feeders.pop(); } delete feederPositionMap[_feeder]; revokeRole(UPDATER_ROLE, _feeder); emit FeederRemoved(_feeder); }
uint8 feederIndex = feederPositionMap[_feeder].index; = feederPositionMap[A].index = 0 if (feederIndex >= 0 && feeders[feederIndex] == _feeder) { // true since feederIndex is 0 feeders[feederIndex] = feeders[feeders.length - 1]; = feeders[2-1] = feeders[1] = B // thus feeders[0] = B feeders.pop(); // This deletes the last feeder, so feeders becomes {B} } delete feederPositionMap[_feeder]; // delete feederPositionMap[A]; revokeRole(UPDATER_ROLE, _feeder); emit FeederRemoved(_feeder);
feeders = {B} feederPositionMap[B].index = 1 // since this was never updated
uint8 feederIndex = feederPositionMap[_feeder].index; = feederPositionMap[B].index; = 1 if (feederIndex >= 0 && feeders[feederIndex] == _feeder) { // fails due to overflow, feeders length is 1 so max index is 0 ... }
Update the index as shown below:
function _removeFeeder(address _feeder) internal onlyWhenFeederExisted(_feeder) { uint8 feederIndex = feederPositionMap[_feeder].index; if (feederIndex >= 0 && feeders[feederIndex] == _feeder) { address feederLast = feeders[feeders.length - 1]; feeders[feederIndex] = feederLast; feeders.pop(); feederPositionMap[feederLast].index = feederIndex; } delete feederPositionMap[_feeder]; revokeRole(UPDATER_ROLE, _feeder); emit FeederRemoved(_feeder); }
#0 - c4-judge
2022-12-20T17:38:51Z
dmvt marked the issue as duplicate of #47
#1 - c4-judge
2023-01-09T15:35:16Z
dmvt changed the severity to 3 (High Risk)
#2 - c4-judge
2023-01-23T15:47:29Z
dmvt marked the issue as satisfactory
🌟 Selected for report: csanuragjain
Also found by: cccz, unforgiven
3171.1169 USDC - $3,171.12
The debt tokens are being transferred before calculating the interest rates. But the interest rate calculation function assumes that debt token has not yet been sent thus the outcome currentLiquidityRate
will be incorrect
executeLiquidateERC20
for a position whose health factor <1function executeLiquidateERC20( mapping(address => DataTypes.ReserveData) storage reservesData, mapping(uint256 => address) storage reservesList, mapping(address => DataTypes.UserConfigurationMap) storage usersConfig, DataTypes.ExecuteLiquidateParams memory params ) external returns (uint256) { ... _burnDebtTokens(liquidationAssetReserve, params, vars); ... }
_burnDebtTokens
function _burnDebtTokens( DataTypes.ReserveData storage liquidationAssetReserve, DataTypes.ExecuteLiquidateParams memory params, ExecuteLiquidateLocalVars memory vars ) internal { ... // Transfers the debt asset being repaid to the xToken, where the liquidity is kept IERC20(params.liquidationAsset).safeTransferFrom( vars.payer, vars.liquidationAssetReserveCache.xTokenAddress, vars.actualLiquidationAmount ); ... // Update borrow & supply rate liquidationAssetReserve.updateInterestRates( vars.liquidationAssetReserveCache, params.liquidationAsset, vars.actualLiquidationAmount, 0 ); }
IERC20(params.liquidationAsset).safeTransferFrom( vars.payer, vars.liquidationAssetReserveCache.xTokenAddress, vars.actualLiquidationAmount );
updateInterestRates
function is called on ReserveLogic.sol#L169function updateInterestRates( DataTypes.ReserveData storage reserve, DataTypes.ReserveCache memory reserveCache, address reserveAddress, uint256 liquidityAdded, uint256 liquidityTaken ) internal { ... ( vars.nextLiquidityRate, vars.nextVariableRate ) = IReserveInterestRateStrategy(reserve.interestRateStrategyAddress) .calculateInterestRates( DataTypes.CalculateInterestRatesParams({ liquidityAdded: liquidityAdded, liquidityTaken: liquidityTaken, totalVariableDebt: vars.totalVariableDebt, reserveFactor: reserveCache.reserveFactor, reserve: reserveAddress, xToken: reserveCache.xTokenAddress }) ); ... }
calculateInterestRates
function on DefaultReserveInterestRateStrategy#L127 contract is made which calculates the interest ratefunction calculateInterestRates( DataTypes.CalculateInterestRatesParams calldata params ) external view override returns (uint256, uint256) { ... if (vars.totalDebt != 0) { vars.availableLiquidity = IToken(params.reserve).balanceOf(params.xToken) + params.liquidityAdded - params.liquidityTaken; vars.availableLiquidityPlusDebt = vars.availableLiquidity + vars.totalDebt; vars.borrowUsageRatio = vars.totalDebt.rayDiv( vars.availableLiquidityPlusDebt ); vars.supplyUsageRatio = vars.totalDebt.rayDiv( vars.availableLiquidityPlusDebt ); } ... vars.currentLiquidityRate = vars .currentVariableBorrowRate .rayMul(vars.supplyUsageRatio) .percentMul( PercentageMath.PERCENTAGE_FACTOR - params.reserveFactor ); return (vars.currentLiquidityRate, vars.currentVariableBorrowRate); }
As we can see in above code, vars.availableLiquidity
is calculated as IToken(params.reserve).balanceOf(params.xToken) +params.liquidityAdded - params.liquidityTaken
But the problem is that debt token is already transferred to xToken
which means xToken
already consist of params.liquidityAdded
. Hence the calculation ultimately becomes (xTokenBeforeBalance+params.liquidityAdded) +params.liquidityAdded - params.liquidityTaken
This is incorrect and would lead to higher vars.availableLiquidity
which ultimately impacts the currentLiquidityRate
Transfer the debt asset post interest calculation
function _burnDebtTokens( DataTypes.ReserveData storage liquidationAssetReserve, DataTypes.ExecuteLiquidateParams memory params, ExecuteLiquidateLocalVars memory vars ) internal { IPToken(vars.liquidationAssetReserveCache.xTokenAddress) .handleRepayment(params.liquidator, vars.actualLiquidationAmount); // Burn borrower's debt token vars .liquidationAssetReserveCache .nextScaledVariableDebt = IVariableDebtToken( vars.liquidationAssetReserveCache.variableDebtTokenAddress ).burn( params.borrower, vars.actualLiquidationAmount, vars.liquidationAssetReserveCache.nextVariableBorrowIndex ); liquidationAssetReserve.updateInterestRates( vars.liquidationAssetReserveCache, params.liquidationAsset, vars.actualLiquidationAmount, 0 ); IERC20(params.liquidationAsset).safeTransferFrom( vars.payer, vars.liquidationAssetReserveCache.xTokenAddress, vars.actualLiquidationAmount ); ... ... }
#0 - c4-judge
2022-12-20T14:14:40Z
dmvt marked the issue as primary issue
#1 - c4-judge
2023-01-09T13:34:41Z
dmvt marked the issue as selected for report
#2 - c4-judge
2023-01-23T20:14:57Z
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
It was observed that removeFeeder
can be called by any user as it is missing any ACL. This means any user could remove all the feeders and hence impact the oracle pricing system (Feeders wont be able to add prices if removed)
removeFeeder
function/// @notice Allows owner to remove feeder. /// @param _feeder feeder to remove function removeFeeder(address _feeder) external onlyWhenFeederExisted(_feeder) { _removeFeeder(_feeder); }
Add a modifier to check that caller is owner (Notice onlyWhenFeederExisted(_feeder) is not required since already called in _removeFeeder function)
function removeFeeder(address _feeder) external onlyRole(DEFAULT_ADMIN_ROLE) { _removeFeeder(_feeder); }
#0 - JeffCX
2022-12-18T04:04:32Z
#1 - c4-judge
2022-12-20T16:58:32Z
dmvt marked the issue as duplicate of #31
#2 - c4-judge
2022-12-20T17:04:41Z
dmvt marked the issue as selected for report
#3 - c4-judge
2023-01-09T14:10:18Z
dmvt changed the severity to 3 (High Risk)
#4 - c4-judge
2023-01-09T14:38:55Z
dmvt marked the issue as not selected for report
#5 - c4-judge
2023-01-23T16:01:56Z
dmvt marked the issue as satisfactory
🌟 Selected for report: csanuragjain
Also found by: Lambda, eierina, joestakey, unforgiven
462.3488 USDC - $462.35
The safeTransfer function Safely transfers tokenId
token from from
to to
, checking first that contract recipients are aware of the ERC721 protocol to prevent tokens from being forever locked. But seems like this safety check got missed in the _safeTransfer
function leading to non secure ERC721 transfers
safeTransferFrom
function (Using NToken contract which implements MintableIncentivizedERC721 contract)function safeTransferFrom( address from, address to, uint256 tokenId, bytes memory _data ) external virtual override nonReentrant { _safeTransferFrom(from, to, tokenId, _data); }
function safeTransferFrom( address from, address to, uint256 tokenId, bytes memory _data ) external virtual override nonReentrant { _safeTransferFrom(from, to, tokenId, _data); } function _safeTransferFrom( address from, address to, uint256 tokenId, bytes memory _data ) internal { require( _isApprovedOrOwner(_msgSender(), tokenId), "ERC721: transfer caller is not owner nor approved" ); _safeTransfer(from, to, tokenId, _data); } function _safeTransfer( address from, address to, uint256 tokenId, bytes memory ) internal virtual { _transfer(from, to, tokenId); }
_transfer
functionfunction _transfer( address from, address to, uint256 tokenId ) internal virtual { MintableERC721Logic.executeTransfer( _ERC721Data, POOL, ATOMIC_PRICING, from, to, tokenId ); }
This is calling MintableERC721Logic.executeTransfer
which simply transfers the asset
In this full flow there is no check to see whether to
address can support ERC721 which fails the purpose of safeTransferFrom
function
Also notice the comment mentions that data
parameter passed in safeTransferFrom is sent to recipient in call but there is no such transfer of data
Add a call to onERC721Received
for recipient and see if the recipient actually supports ERC721
#0 - c4-judge
2022-12-20T17:56:56Z
dmvt marked the issue as duplicate of #51
#1 - c4-judge
2022-12-20T17:58:09Z
dmvt marked the issue as selected for report
#2 - c4-judge
2023-01-23T16:16:21Z
dmvt marked the issue as satisfactory
#3 - C4-Staff
2023-02-01T19:10:09Z
captainmangoC4 marked the issue as selected for report
🌟 Selected for report: xiaoming90
Also found by: csanuragjain, unforgiven
731.7962 USDC - $731.80
The ApeStakingLogic
library is not handling the withdraw correctly and will transfer user any existing balance held by the contract in addition to user requested amount.
Assume initial NToken ApeCoin balance is 1000 at NTokenMAYC
contract (Also true for NTokenBAYC
)
User call withdrawBAKC
function at PoolApeStaking#L120
function withdrawBAKC( address nftAsset, ApeCoinStaking.PairNftWithAmount[] memory _nftPairs ) external nonReentrant { ... nTokenApeStaking.withdrawBAKC(_nftPairs, msg.sender); ... }
withdrawBAKC
function call to NTokenMAYC
contract (Assume passed Paired argument contained MAYC NFT's)function withdrawBAKC( ApeCoinStaking.PairNftWithAmount[] memory _nftPairs, address _apeRecipient ) external onlyPool nonReentrant { ApeStakingLogic.withdrawBAKC( _apeCoinStaking, POOL_ID(), _nftPairs, _apeRecipient ); }
ApeStakingLogic
function withdrawBAKC
which is implemented as below:function withdrawBAKC( ApeCoinStaking _apeCoinStaking, uint256 poolId, ApeCoinStaking.PairNftWithAmount[] memory _nftPairs, address _apeRecipient ) external { ... if (poolId == BAYC_POOL_ID) { _apeCoinStaking.withdrawBAKC(_nftPairs, _otherPairs); } else { _apeCoinStaking.withdrawBAKC(_otherPairs, _nftPairs); } uint256 balance = _apeCoinStaking.apeCoin().balanceOf(address(this)); _apeCoinStaking.apeCoin().safeTransfer(_apeRecipient, balance); }
withdrawBAKC
on the contract ApeCoinStaking
function withdrawBAKC(PairNftWithAmount[] calldata _baycPairs, PairNftWithAmount[] calldata _maycPairs) external { updatePool(BAKC_POOL_ID); _withdrawPairNft(BAYC_POOL_ID, _baycPairs); _withdrawPairNft(MAYC_POOL_ID, _maycPairs); } function _withdrawPairNft(uint256 mainTypePoolId, PairNftWithAmount[] calldata _nfts) private { ... _withdraw(BAKC_POOL_ID, position, amount, mainTokenOwner); ... } function _withdraw(uint256 _poolId, Position storage _position, uint256 _amount, address _recipient) private { ... apeCoin.safeTransfer(_recipient, _amount); }
NTokenMAYC
contract receives the withdrawn apeCoin say X amount. The final balance of contract becomes now 1000+X
Finally the last logic at withdrawBAKC function at ApeStakingLogic
executes
uint256 balance = _apeCoinStaking.apeCoin().balanceOf(address(this)); _apeCoinStaking.apeCoin().safeTransfer(_apeRecipient, balance);
Keep hold of initial apeCoin balance before calling _apeCoinStaking.withdrawBAKC
. Subtract the final balance with this initial balance to get the correct amount to send
function withdrawBAKC( ApeCoinStaking _apeCoinStaking, uint256 poolId, ApeCoinStaking.PairNftWithAmount[] memory _nftPairs, address _apeRecipient ) external { ApeCoinStaking.PairNftWithAmount[] memory _otherPairs = new ApeCoinStaking.PairNftWithAmount[](0); uint256 initialBalance = _apeCoinStaking.apeCoin().balanceOf(address(this)); if (poolId == BAYC_POOL_ID) { _apeCoinStaking.withdrawBAKC(_nftPairs, _otherPairs); } else { _apeCoinStaking.withdrawBAKC(_otherPairs, _nftPairs); } uint256 balance = _apeCoinStaking.apeCoin().balanceOf(address(this)); _apeCoinStaking.apeCoin().safeTransfer(_apeRecipient, balance-initialBalance); }
#0 - c4-judge
2022-12-20T14:09:49Z
dmvt marked the issue as primary issue
#1 - c4-judge
2023-01-23T20:05:28Z
dmvt marked the issue as satisfactory
#2 - trust1995
2023-01-26T20:12:45Z
Contract is behaving as intended. NTokens should not hold ERC20 tokens such as APE. The premise stated in bullet point 1 is false. When ApeStakingLogic's withdrawBAKC is called, it is running in the context of an Ape NToken. Therefore, no harm is done, and whatever APEcoins we've managed to withdraw are safely sent to the recipient.
EDIT: After looking at the edge case scenario explained in dup report, I still believe protocol is working as intended. It is user's responsibility to withdraw APE yield before selling a part of a BAYC/MAYC pair. It is a known functionality of Ape staking that transfer of the Ape means transfer of underlying yield, and is usually priced in.
#3 - csanuragjain
2023-01-27T07:34:41Z
Actually NToken are holding APE coins.
Mainnet Example (1712 APE coin): NTokenBAYC - https://etherscan.io/address/0xdb5485C85Bd95f38f9def0cA85499eF67dC581c0
It has already applied before-after balance check and thus prevent excess withdrawal
#4 - dmvt
2023-01-27T10:15:24Z
It should also be noted that privately the sponsor confirmed this issue, although thinks it should be rated medium risk.
#5 - trust1995
2023-01-27T10:22:47Z
Would really like judge to relate to the argument laid in previous comment and explain the chosen severity in that perspective.
#6 - c4-judge
2023-01-27T10:39:42Z
dmvt changed the severity to 2 (Med Risk)
#7 - C4-Staff
2023-02-01T19:10:20Z
captainmangoC4 marked issue #284 as primary and marked this issue as a duplicate of 284
🌟 Selected for report: xiaoming90
Also found by: cccz, csanuragjain, imare, unforgiven
355.653 USDC - $355.65
The current implementation of NTokenMoonBirds
restricts it to receive any airdrop. This is because onERC721Received
only allows MoonBird contract as sender of NFT which means any other sender is plainly rejected
safeTransferFrom
functiononERC721Received
method on receiving contract which in this case is NTokenMoonBirds
contractfunction 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); ... }
The hardrcore requirement for sender can be removed and contract should allow airdrop NFT
function onERC721Received( address operator, address from, uint256 id, bytes memory ) external virtual override returns (bytes4) { // 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; } // only accept MoonBird tokens 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-20T14:14:02Z
dmvt marked the issue as primary issue
#1 - c4-judge
2023-01-23T20:10:57Z
dmvt marked the issue as satisfactory
#2 - C4-Staff
2023-02-01T19:10:30Z
captainmangoC4 marked issue #286 as primary and marked this issue as a duplicate of 286
266.7397 USDC - $266.74
Judge has assessed an item in Issue #229 as M risk. The relevant finding follows:
Support for IERC165 interface id is missed Contract: https://github.com/code-423n4/2022-11-paraspace/blob/main/paraspace-core/contracts/protocol/tokenization/base/MintableIncentivizedERC721.sol#L572
Impact: Contract fails to support a valid interface which could lead to failure of genuine calls
Steps:
Observe the supportsInterface function supportsInterface(bytes4 interfaceId) external view virtual override(IERC165) returns (bool) { return interfaceId == type(IERC721Enumerable).interfaceId || interfaceId == type(IERC721Metadata).interfaceId; } Observe that support for IERC165 interface id is missing Recommendation: Kindly revise the function as below:
function supportsInterface(bytes4 interfaceId) external view virtual override(IERC165) returns (bool) { return interfaceId == type(IERC721Enumerable).interfaceId || interfaceId == type(IERC721Metadata).interfaceId || interfaceId == type(IERC165).interfaceId; }
#0 - c4-judge
2023-01-25T15:44:23Z
dmvt marked the issue as duplicate of #52
#1 - c4-judge
2023-01-25T15:44:29Z
dmvt marked the issue as satisfactory
🌟 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
Impact:
The transferFrom
allows an approved user to transfer NFT on behalf of owner. But seems like this feature is broken since one of its internal call requires caller to be owner only which means the call would fail
Steps:
transferFrom
functionfunction transferFrom( address from, address to, uint256 tokenId ) external virtual override nonReentrant { //solhint-disable-next-line max-line-length require( _isApprovedOrOwner(_msgSender(), tokenId), "ERC721: transfer caller is not owner nor approved" ); _transfer(from, to, tokenId); }
_transfer
function. Since this is an abstract contract which is implemented by NToken
contract. Now NToken
has overridden its own _transfer
functionfunction _transfer( address from, address to, uint256 tokenId, bool validate ) internal virtual { address underlyingAsset = _underlyingAsset; uint256 fromBalanceBefore = collateralizedBalanceOf(from); uint256 toBalanceBefore = collateralizedBalanceOf(to); bool isUsedAsCollateral = _transferCollateralizable(from, to, tokenId); ... }
_transferCollateralizable
which internally calls executeTransfer
functionunction _transferCollateralizable( address from, address to, uint256 tokenId ) internal virtual returns (bool isUsedAsCollateral_) { isUsedAsCollateral_ = MintableERC721Logic .executeTransferCollateralizable( _ERC721Data, POOL, ATOMIC_PRICING, from, to, tokenId ); } function executeTransferCollateralizable( ... ) external returns (bool isUsedAsCollateral_) { ... executeTransfer(erc721Data, POOL, ATOMIC_PRICING, from, to, tokenId); }
executeTransfer
function checks whether caller is owner itself. Since caller is an approved user and not owner so this call failsfunction executeTransfer( MintableERC721Data storage erc721Data, IPool POOL, bool ATOMIC_PRICING, address from, address to, uint256 tokenId ) public { require( erc721Data.owners[tokenId] == from, "ERC721: transfer from incorrect owner" ); ... }
Recommendation: Either remove the owner requirement or b) if this is meant to be called only by owner then remove the approved user allow
Impact:
It seems the poolAdmin
holds too much power including changing reward controller, rescue tokens etc. This can allow poolAdmin to impact all users by changing the config or draining the contract. In this example we will see one example for setIncentivesController
Steps:
setIncentivesController
and set rewardController
to zeroRecommendation:
Keep the poolAdmin
as multiSig and behind timelock to prevent immediate changes
Impact:
Asset are deleted directly leaving blanks in middle of assets
. This could have unintended effects based on how assets
is being utlized
Steps:
removeAsset
function is called to remove asset A
_removeAsset
functionfunction _removeAsset(address _asset) internal onlyWhenAssetExisted(_asset) { uint8 assetIndex = assetFeederMap[_asset].index; = assetFeederMap[A].index = 0 delete assets[assetIndex]; = delete assets[0]; delete assetPriceMap[_asset]; delete assetFeederMap[_asset]; emit AssetRemoved(_asset); }
assets[0] = {} assets[1] = {B}
Recommendation:
Instead of directly deleting the asset, swap deleted asset index with last index asset and then pop last element. Sample example is _removeFeeder
Impact: Contract fails to support a valid interface which could lead to failure of genuine calls
Steps:
function supportsInterface(bytes4 interfaceId) external view virtual override(IERC165) returns (bool) { return interfaceId == type(IERC721Enumerable).interfaceId || interfaceId == type(IERC721Metadata).interfaceId; }
Recommendation: Kindly revise the function as below:
function supportsInterface(bytes4 interfaceId) external view virtual override(IERC165) returns (bool) { return interfaceId == type(IERC721Enumerable).interfaceId || interfaceId == type(IERC721Metadata).interfaceId || interfaceId == type(IERC165).interfaceId; }
#0 - c4-judge
2023-01-25T15:44:33Z
dmvt marked the issue as grade-b