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: 14/109
Findings: 3
Award: $2,383.77
🌟 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
1858.2053 USDC - $1,858.21
mintLegendaryGobbler() burn Gobbler only set the owner=address(0), and did not clear getApproved(id), resulting in the transferFrom can be called to transfer back to old owner
Suppose users[0] has Gobbler(1) steps: step 1:users[0] call approve(users[1],1); step 2:users[0] call mintLegendaryGobbler([1]); step 3:users[1] call transferFrom(address(0),users[0],1)
After that users[0] get Gobbler(1) back
function mintLegendaryGobbler(uint256[] calldata gobblerIds) external returns (uint256 gobblerId) { .. 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"); burnedMultipleTotal += getGobblerData[id].emissionMultiple; //************ don't delete getApproved[id]****************// emit Transfer(msg.sender, getGobblerData[id].owner = address(0), id); } ..
function transferFrom( address from, address to, uint256 id ) public override { require(from == getGobblerData[id].owner, "WRONG_FROM"); require(to != address(0), "INVALID_RECIPIENT"); //********* getApproved[id] still valid ************// require( msg.sender == from || isApprovedForAll[from][msg.sender] || msg.sender == getApproved[id], "NOT_AUTHORIZED" ); delete getApproved[id]; getGobblerData[id].owner = to; unchecked { //********* unchecked so getUserData[address(0)].gobblersOwned -=1 underflow is ok ************// uint32 emissionMultiple = getGobblerData[id].emissionMultiple; // Caching saves gas. // We update their last balance before updating their emission multiple to avoid // penalizing them by retroactively applying their new (lower) emission multiple. getUserData[from].lastBalance = uint128(gooBalance(from)); getUserData[from].lastTimestamp = uint64(block.timestamp); getUserData[from].emissionMultiple -= emissionMultiple; getUserData[from].gobblersOwned -= 1; ... ... } emit Transfer(from, to, id); } }
function mintLegendaryGobbler(uint256[] calldata gobblerIds) external returns (uint256 gobblerId) { .. 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"); burnedMultipleTotal += getGobblerData[id].emissionMultiple; +++ delete getApproved[id]; emit Transfer(msg.sender, getGobblerData[id].owner = address(0), id); } ..
#0 - Shungy
2022-09-27T19:18:54Z
#1 - GalloDaSballo
2022-10-02T14:12:57Z
🌟 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
1: ArtGobblers.sol may not be able to set a new RandProvider
if current VRF is sunset or invalid will change RandProvider by call upgradeRandProvider() upgradeRandProvider() detect gobblerRevealsData.waitingForSeed!=true But it is very possible that the old RandProvider is no longer valid and can no longer provide randomSeed again, resulting in waitingForSeed always being true, thus making it impossible to change the RandProvider Suggest adding an expiration time
function upgradeRandProvider(RandProvider newRandProvider) external onlyOwner { // Revert if waiting for seed, so we don't interrupt requests in flight. --- if (gobblerRevealsData.waitingForSeed) revert SeedPending(); +++ if (gobblerRevealsData.waitingForSeed) { +++ if (block.timestamp <= gobblerRevealsData.nextRevealTimestamp + 1 days) revert SeedPending(); +++ gobblerRevealsData.waitingForSeed = false; +++ gobblerRevealsData.toBeRevealed = 0; } randProvider = newRandProvider; // Update the randomness provider. emit RandProviderUpgraded(msg.sender, newRandProvider); } ``` 2: ChainlinkV1RandProvider.sol event use the wrong requestId requestRandomBytes() first emit then set requestId ``` function requestRandomBytes() external returns (bytes32 requestId) { // The caller must be the ArtGobblers contract, revert otherwise. if (msg.sender != address(artGobblers)) revert NotGobblers(); //********** requestId always eq 0 ***********// emit RandomBytesRequested(requestId); // Will revert if we don't have enough LINK to afford the request. return requestRandomness(chainlinkKeyHash, chainlinkFee); } ``` Recommend: ``` function requestRandomBytes() external returns (bytes32 requestId) { .... +++ requestId = requestRandomness(chainlinkKeyHash, chainlinkFee); emit RandomBytesRequested(requestId); // Will revert if we don't have enough LINK to afford the request. --- return requestRandomness(chainlinkKeyHash, chainlinkFee); } ``` 3: ArtGobblers.sol show burnd gobbler's tokenURI If it has been burned it should prompt NOT_MINTED ``` function tokenURI(uint256 gobblerId) public view virtual override returns (string memory) { // Between 0 and lastRevealed are revealed normal gobblers. if (gobblerId <= gobblerRevealsData.lastRevealedId) { --- if (gobblerId == 0) revert("NOT_MINTED"); // 0 is not a valid id for Art Gobblers. +++ if (gobblerId == 0 || getGobblerData[gobblerId].owner==address(0)) revert("NOT_MINTED"); return string.concat(BASE_URI, uint256(getGobblerData[gobblerId].idx).toString()); } ``` L-04: ArtGobblers.sol gobblerRevealsData.nextRevealTimestamp is used to limit the time between two reveals to not less than 1 day However, when setting the next time, use the last time + 1 days, but if no one mint Gobbler, then the last time will be much smaller than yesterday, especially at the beginning It is recommended to change it to the current time + 1 days ``` function requestRandomSeed() external returns (bytes32) { ... --- gobblerRevealsData.nextRevealTimestamp = uint64(nextRevealTimestamp + 1 days); +++ gobblerRevealsData.nextRevealTimestamp = uint64(block.timestamp + 1 days); ```
#0 - GalloDaSballo
2022-10-06T00:41:54Z
Dup of #153
Valid NC
Disagree
R
#1 - GalloDaSballo
2022-10-13T23:37:08Z
1R 1NC