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: 163/243
Findings: 3
Award: $0.62
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: btk
Also found by: 00xSEV, 0x175, 0x180db, 0x3b, 0xAlix2, 0xJuda, 0xpiken, 0xraion, 3th, 836541, Al-Qa-qa, AvantGard, Aymen0909, Beosin, ChrisTina, DarkTower, DeFiHackLabs, EricWWFCP, Kose, Kow, KupiaSec, MrPotatoMagic, Neo_Granicen, PENGUN, PetarTolev, Ruhum, Soul22, SovaSlava, SpicyMeatball, Talfao, The_Kakers, Toshii, Tricko, VAD37, Viktor_Cortess, ZdravkoHr, _eperezok, alexxander, audityourcontracts, ayden, bird-flu, bronze_pickaxe, codynhat, critical-or-high, danielles0xG, degensec, droptpackets, evmboi32, fibonacci, flacko, gumgumzum, ilchovski, immeas, innertia, jacopod, joesan, ke1caM, kk_krish, mojito_auditor, nuthan2x, phoenixV110, pontifex, r0ck3tz, sces60107, seeques, sl1, smiling_heretic, stackachu, t0x1c, trachev, turvy_fuzz, ubl4nk, ustas, xAriextz, xuwinnie, y4y
0.152 USDC - $0.15
https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/MinterContract.sol#L196-L253 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/NextGenCore.sol#L189-L200 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/NextGenCore.sol#L227-L232
Both allow list & public users can circumvent the max allowance of tokens allowed to be minted per address. Merkle tree allowance is bypassed.
The allowlist case is considered since a merkle tree bounds the address space (in public minting anyone can just mint from different addresses, although, circumvanting turns out to be possible on per address basis as well).
require(_maxAllowance >= gencore.retrieveTokensMintedALPerAddress(col, msg.sender) + _numberOfTokens, "AL limit");
The problem is that the minter per address counter in NextGenCore
contract is updated after the NFT is minted in _mintProcessing(...)
_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; }
This is dangerous since a malicious user can use the onERC721Received
hook to reenter mint(...)
in Minter
contract before the tokensMintedAllowlistAddress
is updated. This ultimately allows the allow list user to mint more tokens than what is stated in the Merkle Tree. The same goes for public minting as well. Also, an interesting caveat is that even if the allow list consists only of EOA addresses, a malicious user can still use the delegate feature to delegate towards a malicious contract that will mint on his behalf.
/test
forge test --match-test testBypassMaxAllowance
Manual Inspection
Mint after the allowance counter update
Context
#0 - c4-pre-sort
2023-11-20T11:20:35Z
141345 marked the issue as duplicate of #51
#1 - c4-pre-sort
2023-11-26T14:04:29Z
141345 marked the issue as duplicate of #1742
#2 - c4-judge
2023-12-08T16:15:54Z
alex-ppg marked the issue as satisfactory
#3 - c4-judge
2023-12-08T16:16:00Z
alex-ppg marked the issue as partial-50
#4 - c4-judge
2023-12-08T19:17:01Z
alex-ppg marked the issue as satisfactory
#5 - c4-judge
2023-12-09T00:18:52Z
alex-ppg changed the severity to 3 (High Risk)
🌟 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
A malicious user can use a Flash Loan to drain all the ongoing auctions except the one he uses to execute the attack from.
This attack is build upon several issues contained within AuctionDemo.sol
. The listed issues have smaller impact on their own, however, this report explains how they can be combined to cause maximum damage.
An auction starts from mintAndAuction(…)
where the NFT is minted to some “Custody” contract that approves to AuctionDemo
contract and auctionEndTime
is passed as the time that the auction will end. From that point anyone can call particpateToAuction(…)
in AuctionDemo
to submit a bid. Users can also cancel their bids or wait till claimAuction(…)
is called when the Auction has ended and everyone, except the winner, will get a refund of their bids, while the winner receives the NFT. It’s also important to note that there is only 1 AuctionDemo
contract that can host arbitrary amount of Auctions for any collection for any duration i.e we have auctions running in parallel.
Issue #1
claimAuction(…)
is available at auctionEndTime
, however, participateToAuction(…)
is also available at auctionEndTime
. This causes a front run concern where whoever is faster to beat the best bid at the auctionEndTime
block.timestamp
can immediately call claimAuction
and win. The only precondition is that auctionEndTime
indeed falls on a block.timestamp
, which is likely since we can have multiple auctions.
function participateToAuction(uint256 _tokenid) public payable { require(msg.value > returnHighestBid(_tokenid) && block.timestamp <= minter.getAuctionEndTime(_tokenid) && minter.getAuctionStatus(_tokenid) == true); // more code ... }
function claimAuction(uint256 _tokenid) public WinnerOrAdminRequired(_tokenid,this.claimAuction.selector){ require(block.timestamp >= minter.getAuctionEndTime(_tokenid) && auctionClaim[_tokenid] == false && minter.getAuctionStatus(_tokenid) == true); }
Issue #2
When refunding bidders, claimAuction(…)
doesn’t update auctionInfoData[_tokenid][index].status
, which means that a bidder can implement a fallback()
function that, upon receiving the bid refund, calls cancelBid(…)
and gets a second refund for the same bid i.e double spending issue. This is under the assumption that claimAuction
is called exactly at auctionEndTime
and auctionEndTime
falls on a block.timestamp
, otherwise, cancelBid(…)
is not available.
else if (auctionInfoData[_tokenid][i].status == true) { (bool success, ) = payable(auctionInfoData[_tokenid][i].bidder).call{value: auctionInfoData[_tokenid][i].bid}(""); emit Refund(auctionInfoData[_tokenid][i].bidder, _tokenid, success, highestBid);
Assuming that auctionEndTime
falls on a block.timestamp
for an auction that's ongoing, a malicious user can formulate the following attack
exploitableBalance
is the balance of the AuctionDemo
contract (i.e all auctions) minus the amount that has to be refunded to the bidders in the auction that the attack is launched fromonERC721Received
hook & a programmed fallback()
function (this can be done prior to the attack block)auctionEndTime
user acquires into his malicious contract a flash loan worth exploitableBalance + a highest bid
(to be able bid & to call claimAuction(...)
)participateToAuction(...)
with a highest bidclaimAuction(...)
, winning the auctionclaimAuction(...)
will now refund any refundees and send the NFT to the malicious contractonERC721Received
is triggered inside the Malicious contract - inside the hook Malicious contract calls participateToAuction(...)
with a new bid that is worth exactly exploitableBalance
claimAuction(...)
invocation where we now have an updated state [auctionInfoData
that is iterated over has one more entry] with one more bid from the malicious contract.claimAuction(...)
proceeds to refund the Malicious contract for exploitableBalance
and enters maliciou's contract fallback()
fallback()
Malicious contract calls cancelBid(…)
and receives a second refund worth exploitableBalance
i.e stealing all the funds inside AuctionDemo
from all other ongoing auctions1*exploitableBalance + highestBid
1*exploitableBalance - highestBid
+ NFTforge init --force
/test
forge test --match-test testRobAuctions -vv
Manual Inspection
Ensure claimAuction(...)
, participateToAuction(...)
& cancelBid(...)
cannot be called at the same block.timestamp
Context
#0 - c4-pre-sort
2023-11-20T11:22:41Z
141345 marked the issue as duplicate of #962
#1 - c4-judge
2023-12-04T21:42:35Z
alex-ppg marked the issue as duplicate of #1323
#2 - c4-judge
2023-12-08T17:40:26Z
alex-ppg marked the issue as satisfactory
🌟 Selected for report: The_Kakers
Also found by: 00decree, 00xSEV, 0x180db, 0x3b, 0xJuda, 0x_6a70, 0xarno, 0xpiken, Arabadzhiev, Bauchibred, BugsFinder0x, BugzyVonBuggernaut, ChrisTina, DeFiHackLabs, Delvir0, HChang26, Haipls, Jiamin, Juntao, KupiaSec, Madalad, Neon2835, Nyx, Ocean_Sky, SpicyMeatball, Talfao, Taylor_Webb, Timenov, Tricko, ZdravkoHr, _eperezok, alexxander, amaechieth, bdmcbri, bronze_pickaxe, circlelooper, crunch, cu5t0mpeo, dimulski, fibonacci, funkornaut, immeas, ke1caM, lsaudit, nuthan2x, r0ck3tz, rotcivegaf, spark, tnquanghuy0512, twcctop, xeros
0.4703 USDC - $0.47
A contract that wins an auction can postpone owner & refundee’s payments indefinitely.
At the time of winning, an Auction holds the owner’s sell fee (equal to what the winning bid is) and also holds extra funds from the non-winning bids. Upon concluding an auction it is expected that the owner & refundees receive their payments. To distribute these fees either an Admin or the winner of the auction has to call claimAuction(...)
. The issue is that a malicious actor can bid & win through a contract that has a reverting onERC721Received()
hook function. In that case claimAuction(...)
will revert when it attempts to safeTransferFrom(...)
the NFT to the malicious bidder. In such a manner a malicious winner can have final authority when and if the owner & refundees get their payments. Note that this does require financial commitment from the malicious winner (i.e he has to win the auction), however the funds that will be withheld will be more than what he commits.
The same idea of bricking an auction’s funds and NFT but at significantly cheaper cost (eg 1 wei + gas fees) can be executed by a malicious bidder that front runs the first bid through a contract that implements a “return bomb” (memory expansion & copy attack) in the fallback()
function - this will cause claimAuction(...)
to revert when it attempts to refund the 1 wei bidder. This finding however will most likely be deemed OOS because of [L-18] in the bot findigs, nevertheless, I am noting it here for sponsor’s & judge reference since it implies severe consequneces.
/test
forge test --match-test testForcedRevert
claimAuction(...)
revertsManual Inspection
Separate the logic of claiming the NFT, paying the owner & refunding refundees.
DoS
#0 - c4-pre-sort
2023-11-14T09:34:22Z
141345 marked the issue as duplicate of #1952
#1 - c4-judge
2023-12-04T18:59:23Z
alex-ppg marked the issue as not a duplicate
#2 - c4-judge
2023-12-04T18:59:43Z
alex-ppg marked the issue as duplicate of #1759
#3 - c4-judge
2023-12-08T22:05:27Z
alex-ppg marked the issue as partial-50