Platform: Code4rena
Start Date: 20/09/2022
Pot Size: $100,000 USDC
Total HM: 4
Participants: 109
Period: 7 days
Judge: GalloDaSballo
Id: 163
League: ETH
Rank: 18/109
Findings: 2
Award: $1,913.41
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: philogy
Also found by: KIntern_NA, auditor0517, bin2chen, cccz, hansfriese, hyh, ladboy233, m9800, pauliax, pedroais, ronnyx2017, wagmi, wastewa, zzykxx
https://github.com/code-423n4/2022-09-artgobblers/blob/d2087c5a8a6a4f1b9784520e7fe75afa3a9cbdbe/src/ArtGobblers.sol#L411 https://github.com/code-423n4/2022-09-artgobblers/blob/d2087c5a8a6a4f1b9784520e7fe75afa3a9cbdbe/src/ArtGobblers.sol#L693 https://github.com/code-423n4/2022-09-artgobblers/blob/d2087c5a8a6a4f1b9784520e7fe75afa3a9cbdbe/src/utils/token/GobblersERC721.sol#L92
Detailed description of the impact of this finding.
When minting legendary NFT, non-legendary NFTs are burned,
only the owner of the burned nft is set to 0,
emit Transfer(msg.sender, getGobblerData[id].owner = address(0), id);
but burned token approval is not revoked,
burned NFT TokenURI is still accessible after burning.
Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.
we update the token uri for testing purpose in the test case
gobblers = new ArtGobblers( keccak256(abi.encodePacked(users[0])), block.timestamp, goo, Pages(pagesAddress), address(team), address(community), randProvider, "revealed/", "not_revealed/" );
We can add the test to ArtGobbler.t.sol
function testMintLegendaryGobbleBurningCleanUp_POC() public { uint256 startTime = block.timestamp + 30 days; vm.warp(startTime); // Mint full interval to kick off first auction. mintGobblerToAddress(users[0], gobblers.LEGENDARY_AUCTION_INTERVAL()); vm.prank(users[0]); gobblers.approve(users[1], 1); uint256 cost = gobblers.legendaryGobblerPrice(); assertEq(cost, 69); setRandomnessAndReveal(cost, "seed"); uint256 emissionMultipleSum; //add more ids than necessary for (uint256 curId = 1; curId <= cost + 10; curId++) { ids.push(curId); assertEq(gobblers.ownerOf(curId), users[0]); emissionMultipleSum += gobblers.getGobblerEmissionMultiple(curId); } vm.prank(users[0]); gobblers.mintLegendaryGobbler(ids); console.log("owner for token id 1 is address(0) after burning"); hevm.expectRevert("NOT_MINTED"); console.log(gobblers.ownerOf(1)); console.log(""); console.log("---- legendary gobbler is minted --- tokenUI is -----"); console.log(gobblers.tokenURI(9991)); console.log(""); console.log(" --- tokenURI for burned token id 1 still accessible --- "); console.log(gobblers.tokenURI(1)); console.log(""); console.log(" --- token approval not revoked after token burning ----"); console.log(" user 1 still got approve for user 0?"); console.log(gobblers.getApproved(1) == users[1]); //check full cost was burned for (uint256 curId = 1; curId <= cost; curId++) { hevm.expectRevert("NOT_MINTED"); gobblers.ownerOf(curId); } // check extra tokens were not burned for (uint256 curId = cost + 1; curId <= cost + 10; curId++) { assertEq(gobblers.ownerOf(curId), users[0]); } }
we run the test
forge test -vv --match testMintLegendaryGobbleBurningCleanUp_POC
the running is
[PASS] testMintLegendaryGobbleBurningCleanUp_POC() (gas: 41308783) Logs: owner for token id 1 is address(0) after burning 0x0000000000000000000000000000000000000000 ---- legendary gobbler is minted --- tokenUI is ----- revealed/9991 --- tokenURI for burned token id 1 still accessible --- revealed/8876 --- token approval not revoked after token burning ---- user 1 still got approve for user 0? true Test result: ok. 1 passed; 0 failed; finished in 55.00ms
note after we mint 69 non-legendary art gobbler nfts,
we approve user 1 to spend nft id 1 on behalf of user 0
vm.prank(users[0]); gobblers.approve(users[1], 1);
then after the burning and minting of the legendary art gobbler, we check
console.log(""); console.log(" --- tokenURI for burned token id 1 still accessible --- "); console.log(gobblers.tokenURI(1)); console.log(""); console.log(" --- token approval not revoked after token burning ----"); console.log(" user 1 still got approve for user 0?"); console.log(gobblers.getApproved(1) == users[1]);
but as the test shown above, burned NFT tokenURI still accessible, token approval is not revoked.
foundry
Manual Review
we recommend the project handle the burning-related logic carefully.
We recommand the project revoke the approval when nft is burned and disable the token metadata access and update
getGobblerData[id]
accordingly.
#0 - Shungy
2022-09-27T19:02:07Z
This finding mentions approve is not deleted, but it does not explicitly mention that the burnt gobbler can be recovered.
Also burnt gobbler tokenURI still being accessible could have been another issue.
Duplicate: https://github.com/code-423n4/2022-09-artgobblers-findings/issues/389
#1 - GalloDaSballo
2022-10-02T14:12:01Z
#2 - GalloDaSballo
2022-10-02T14:12:32Z
Saved by this statement
🌟 Selected for report: IllIllI
Also found by: 0x1f8b, 0x4non, 0x52, 0x5rings, 0xNazgul, 0xRobocop, 0xSmartContract, 0xdeadbeef, 0xsanson, 8olidity, Amithuddar, Aymen0909, B2, B353N, CertoraInc, Ch_301, Chom, CodingNameKiki, Deivitto, ElKu, Funen, JC, JohnnyTime, Kresh, Lambda, Noah3o6, RaymondFam, ReyAdmirado, RockingMiles, Rolezn, Sm4rty, SuldaanBeegsi, Tadashi, TomJ, Tomio, V_B, Waze, __141345__, a12jmx, ak1, arcoun, asutorufos, aviggiano, berndartmueller, bharg4v, bin2chen, brgltd, bulej93, c3phas, catchup, cccz, ch0bu, cryptonue, cryptphi, csanuragjain, delfin454000, devtooligan, djxploit, durianSausage, eighty, erictee, exd0tpy, fatherOfBlocks, giovannidisiena, hansfriese, ignacio, joestakey, ladboy233, lukris02, m9800, malinariy, martin, minhtrng, obront, oyc_109, pedr02b2, pedroais, pfapostol, philogy, prasantgupta52, rbserver, ronnyx2017, rotcivegaf, rvierdiiev, sach1r0, shung, simon135, throttle, tnevler, tonisives, wagmi, yixxas, zkhorse, zzykxx, zzzitron
55.1985 USDC - $55.20
All contracts use the pragma version
// SPDX-License-Identifier: AGPL-3.0-only pragma solidity >=0.8.0;
the customized error are extensively used in the code
error InvalidProof(); error AlreadyClaimed(); error MintStartPending();
(customized error in ArbGobbler.sol)
however, customized errors are not supported until solidity version 0.8.4
https://github.com/ethereum/solidity/releases/tag/v0.8.4
So using the solidity version between 0.8.4 and 0.8.0 can result in compiler error.
We recommand the project use the up-to-date solidity version.
// SPDX-License-Identifier: AGPL-3.0-only pragma solidity >=0.8.4;
https://ethereum.stackexchange.com/questions/115280/mint-vs-safemint-which-is-best-for-erc721
If YOU are paying for the minting of tokens, use _mint. The _safeMint might cost you an arbitrary amount of money because of choices made by the recipient of the tokens. This is enough to deter you from considering it.
If THEY are paying for the minting of tokens and you expect buyers to be composing functionality with smart contracts, use _safeMint. There is some marginal benefit of allowing the extra features with smart contracts this allows.
the Page NFT do not have owner, which means it cannot be listed on opensea or looksRare as collection
We recommand the project add owner for Page NFT.
https://docs.opensea.io/docs/contract-level-metadata
We recommand the project add contractURI method to ERC721 or ERC1155 contract that returns a URL for the storefront-level metadata for your contract.
in the function Gobble
function gobble( uint256 gobblerId, address nft, uint256 id, bool isERC1155 ) external { // Get the owner of the gobbler to feed. address owner = getGobblerData[gobblerId].owner; // The caller must own the gobbler they're feeding. if (owner != msg.sender) revert OwnerMismatch(owner);
note the check
if (owner != msg.sender) revert OwnerMismatch(owner);
it only check if the owner is the msg.sender, but does not check if msg.sender is approved by NFT owner like other transfer function is handled,
we recommend the project add the check below
if(owner != msg.sender || !isApprovedForAll(owner, msg.sender) || getApproved(tokenId) != msg.sender) revert OwnerMismatch(owner);
GobblerReserver.sol is minted to receive Art Gobbler ERC721 token.
It is a good practice for a smart contract to implement onERC721Received hook
in safeTransfer is used.
https://docs.openzeppelin.com/contracts/3.x/api/token/erc721
we recommand change the line
uint256 newNumMintedForReserves = numMintedForReserves += (numGobblersEach << 1);
to
uint256 newNumMintedForReserves = numMintedForReserves += (numGobblersEach * 2);
instead of
unchecked { // Overflow should be impossible due to supply cap of 10,000. emit GobblerClaimed(msg.sender, gobblerId = ++currentNonLegendaryId); }
we recommand move ++currentNonLegenaryId before the event mission for better readability, conciseness and consistency.
unchecked { // Overflow should be impossible due to supply cap of 10,000. gobblerId = ++currentNonLegendaryId emit GobblerClaimed(msg.sender, gobblerId); }
same for the event mission in mintLegendary
function mintLegendaryGobbler
we can change
emit Transfer(msg.sender, getGobblerData[id].owner = address(0), id);
to
delete getGobblerData[id]; emit Transfer(msg.sender, address(0), id);
instead of
for (uint256 i = 0; i < numPages; i++) _mint(community, ++lastMintedPageId);
when minting page,
we recommand the project to use batchMint function here.
#0 - GalloDaSballo
2022-10-06T20:15:53Z
NC
L
Disagree
Valid R
R
R
R
Disagree as it's subjective and the event is going to trigger a false positive by slither as well
R
#1 - GalloDaSballo
2022-10-14T19:10:39Z
1L 5R 1NC