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: 6/109
Findings: 4
Award: $5,299.91
🌟 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
https://github.com/code-423n4/2022-09-artgobblers/blob/main/src/ArtGobblers.sol#L411
The function mintLegendaryGobbler()
in ArtGobblers.sol
burns gobblers by transfering them to address(0)
without deleting previous approvals, which allows an user to:
gobblers.approve()
to approve himself for every gobbler he owns and wants to burngobblers.mintLegendaryGobbler()
passing the ids of the self-approved gobblersgobblers.transferFrom()
on every burned gobbler to transfer them from address(0)
to himself
Allows an user to get assets from an account they are not supposed to have access to, address(0). By doing this its possible to gain an unfair advantage in terms of goo multiplier, which lowers the network share of other partecipants, and gobblers owned, which lowers exclusivity.
Copy the test in ArtGobblers.t.sol
and run it with forge test -m testMintLegendaryGobblerAndGetGobblersBack
:
function testMintLegendaryGobblerAndGetGobblersBack() 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(); setRandomnessAndReveal(cost, "seed"); vm.prank(users[0]); /* Approve users[0] as a spender for gobbler 69 */ gobblers.approve(users[0], 69); for (uint256 curId = 1; curId <= cost; curId++) { ids.push(curId); } /* Mint legendary gobbler by burning gobblers with ids [0..=69] */ vm.prank(users[0]); gobblers.mintLegendaryGobbler(ids); /* Gobbler 69 belongs now to address(0) */ (address owner,,) = gobblers.getGobblerData(69); assertEq(owner, address(0)); /* Transfer gobbler 69 from address(0) to users[0] */ vm.prank(users[0]); gobblers.transferFrom(address(0), users[0], 69); /* Gobbler 69 is now back in business */ (owner,,) = gobblers.getGobblerData(69); assertEq(owner, users[0]); }
Deleting approvals of burned gobblers in mintLegendaryGobbler()
fixes the issue:
//...snip... for (uint256 i = 0; i < cost; ++i) { id = gobblerIds[i]; if (id >= FIRST_LEGENDARY_GOBBLER_ID) { revert CannotBurnLegendary(id); } require(getGobblerData[id].owner == msg.sender, "WRONG_FROM"); delete getApproved[id]; //<-- /// DELETE APPROVAL /// burnedMultipleTotal += getGobblerData[id].emissionMultiple; emit Transfer(msg.sender, getGobblerData[id].owner = address(0), id); } //...snip...
#0 - Shungy
2022-09-27T20:27:59Z
#1 - GalloDaSballo
2022-10-02T14:11:58Z
🌟 Selected for report: zzykxx
Also found by: 8olidity, IllIllI, Lambda, berndartmueller, bytehat, devtooligan, hansfriese, imare, obront, philogy, shung, tonisives, zdhu, zkhorse
611.4657 USDC - $611.47
https://github.com/artgobblers/art-gobblers/blob/master/src/ArtGobblers.sol#L562
It could become impossible to reveal gobblers which leads any new minted gobbler to have an emissionMultiple
of 0
forever, preventing them from minting any goo.
In ArtGobblers.sol
calling requestRandomSeed()
sets gobblerRevealsData.waitingForSeed = true
, which makes both revealGobblers()
and upgradeRandProvider()
revert.
The only way to set gobblerRevealsData.waitingForSeed = false
is by calling acceptRandomSeed()
which can only be called by the randProvider
itself.
This means that if randProvider
stops working and requestRandomSeed()
is called (which sets gobblerRevealsData.waitingForSeed = true
) there is no way for upgradeRandProvider()
and revealGobblers()
to be ever called again.
Copy the test in RandProvider.t.sol
and run it with forge test -m testRandomnessBrick
:
function testRandomnessBrick() public { vm.warp(block.timestamp + 1 days); mintGobblerToAddress(users[0], 1); bytes32 requestId = gobblers.requestRandomSeed(); /* At this point we should have vrfCoordinator.callBackWithRandomness(requestId, randomness, address(randProvider)); But that doesn't happen because we are assuming randProvider stopped responding /* /* Can't reveal gobblers */ vm.expectRevert(ArtGobblers.SeedPending.selector); gobblers.revealGobblers(1); /* Can't update provider */ RandProvider newRandProvider = new ChainlinkV1RandProvider( ArtGobblers(address(gobblers)), address(vrfCoordinator), address(linkToken), keyHash, fee ); vm.expectRevert(ArtGobblers.SeedPending.selector); gobblers.upgradeRandProvider(newRandProvider); }
A potential fix is to reset some variables in upgradeRandProvider
instead of reverting:
function upgradeRandProvider(RandProvider newRandProvider) external onlyOwner { if (gobblerRevealsData.waitingForSeed) { gobblerRevealsData.waitingForSeed = false; gobblerRevealsData.toBeRevealed = 0; gobblerRevealsData.nextRevealTimestamp = uint64(nextRevealTimestamp - 1 days); } randProvider = newRandProvider; // Update the randomness provider. emit RandProviderUpgraded(msg.sender, newRandProvider); }
This gives a little extra powers to the owner, but I don't think it's a big deal considering that he can just plug in any provider he wants anyway.
#0 - Shungy
2022-09-28T16:03:18Z
#1 - GalloDaSballo
2022-10-03T20:13:31Z
Making this primary
#2 - GalloDaSballo
2022-10-03T20:16:01Z
The Warden has highlighted a risk with the state handling that concerns receiving randomness from the randProvider
.
As detailed, if no callback is performed, this would put the contract in a state where the faulty provider could not be replaced.
While no specific way for the provider to be bricked was given, if we assume that the provider was retired or deprecated, the contract may fall in such a situation.
Because this is contingent on an externality, but would deny a core functionality of the protocol, I agree with Medium Severity
#3 - FrankieIsLost
2022-10-23T06:21:59Z
Agree, thanks. Fixed here: https://github.com/artgobblers/art-gobblers/pull/154
🌟 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
In ArtGobblers.sol
the function setOwner
sets the owner variable directly, meaning that passing the wrong address could lead to the contract having an owner which is a dead address.
No more gobblers can be revealed if the owner is a dead address and randProvider
stops working.
Implementing a 2-step ownership transfer where the future owner has to accept the ownership by making a call to ArtGobblers.sol
might reduce stress levels by quite a bit when and if ownership has to be transferred.
This same reasoning applies to GobblerReserve.sol
, which in case of a dead address as owner could cause a loss up to 20% of gobblers and 10% of pages.
Submitting as low risk because of heavy external requirements.
At ArtGobblers.sol#L458 a 1
is added to gobblersOwned
to factor in the legendary, but then _mint()
adds 1
again, meaning that the variable gobblersOwned
is off by 1
per legendary gobbler minted. This is not exploitable, but could lead to problems in external projects that use balanceOf
to get an address gobblers balance.
At ArtGobblers.sol#L461 start price is truncated to 120 bits instead of 128, not exploitable and likely to be a typo.
At ArtGobblers.sol#L535 the nextRevealTimestamp
variable is set to the current nextRevealTimestamp + 1 day
, but this can still result in a past timestamp if requestRandomSeed()
wasn't previously called in the past 24 hours
. Could be considered a design choice, but it's nice to be aware of it. Not exploitable per my findings.
The function mintLegendaryGobbler()
never sets the idx of the minted legendary gobbler.
Most of the in-scope contracts require solidity compiler to be >=0.8.0
, as per SWC consider using a fixed compiler version.
#0 - GalloDaSballo
2022-10-06T19:23:31Z
NC
TODO Raise #333
R
L
Believe this to be informational as IDX is not used for Legendaries NC
NC
#1 - GalloDaSballo
2022-10-13T23:52:52Z
1L 1R 3NC