Platform: Code4rena
Start Date: 03/05/2022
Pot Size: $30,000 USDC
Total HM: 6
Participants: 93
Period: 3 days
Judge: gzeon
Id: 118
League: ETH
Rank: 9/93
Findings: 4
Award: $1,211.75
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 Selected for report: VAD37
Also found by: AuditsAreUS, IllIllI, MaratCerby, rfa, sorrynotsorry
1116.8018 USDC - $1,116.80
https://github.com/code-423n4/2022-05-runes/blob/060b4f82b79c8308fe65674a39a07c44fa586cd3/contracts/ForgottenRunesWarriorsGuild.sol#L173-L176 https://github.com/code-423n4/2022-05-runes/blob/060b4f82b79c8308fe65674a39a07c44fa586cd3/contracts/ForgottenRunesWarriorsMinter.sol#L627-L630
Token like USDT known for using non-standard ERC20. (Missing return boolean on transfer).
Contract function forwardERC20 will always revert when try to transfer this kind of tokens.
Cannot withdraw some special ERC20 token through contract call. Unexpected contract functionality = Medium severity
Use SafeTransferLib.safeTransfer instead of IERC20 transfer. This accepts ERC20 token with no boolean return like USDT.
#0 - cryppadotta
2022-05-05T02:08:38Z
Ah nice. You learn something new every day. Thanks!
#1 - KenzoAgada
2022-06-06T07:15:01Z
Description is pretty much invalid as "forwardERC20 will always revert when try to transfer this kind of tokens" is simply not true. Same with impact - "Cannot withdraw some special ERC20 token through contract call" - that's not the impact, using SafeERC20's transfer will not help to transfer tokens. It will just revert on failure. But generally the issue of not using SafeERC20 is kinda-correct. Duplicate of #2.
#2 - gzeoneth
2022-06-18T17:03:28Z
This is not a duplicate of #2. #2 describe the silent failure of ERC20 transfer, while this describe a ERC20 that return void instead of bool. The call will revert even if the transfer is successful because Solidity expected a return value. Judging as Med Risk because unlike #2, here you can actually do something to fix the function.
#3 - KenzoAgada
2022-06-19T06:47:48Z
I apologize, my mistake.
48.5872 USDC - $48.59
Judge has assessed an item in Issue #72 as Medium risk. The relevant finding follows:
With the exception of issueRefund
, other transfer should allow forward all gas()
to finish the transaction. To prevent case of costing more than 23000 gas for transfer.
#0 - gzeoneth
2022-06-18T19:19:23Z
Duplicate of #254
🌟 Selected for report: defsec
Also found by: 0v3rf10w, 0x1f8b, 0x4non, 0x52, 0xDjango, 0xf15ers, 0xkatana, 0xliumin, AuditsAreUS, BowTiedWardens, CertoraInc, Cr4ckM3, Funen, GimelSec, Hawkeye, IllIllI, Kulk0, M0ndoHEHE, MaratCerby, Picodes, Ruhum, TerrierLover, TrungOre, VAD37, WatchPug, berndartmueller, broccolirob, catchup, cccz, cryptphi, csanuragjain, delfin454000, dirk_y, eccentricexit, ellahi, fatherOfBlocks, gzeon, hake, hansfriese, hickuphh3, horsefacts, hubble, hyh, ilan, joestakey, kebabsec, kenta, kenzo, leastwood, m9800, marximimus, minhquanym, oyc_109, p4st13r4, pauliax, pedroais, peritoflores, plotchy, rajatbeladiya, reassor, rfa, robee, rotcivegaf, samruna, shenwilly, shung, simon135, sorrynotsorry, sseefried, teddav, throttle, tintin, unforgiven, z3s
30.8695 USDC - $30.87
Quote from README:
- The times all need to be set correctly - The `finalPrice` of the Dutch Auction is used in subsequent phases and it set automatically _if_ the DA sells out. If the DA fails to sell out, the owner needs to set this value manually for the subsequent phases to be priced correctly.
Trust multisig owner
as sensible actor and all centralization risks here are low and can only happen due to accident or unintended behaviour.
TOC
setFinalPrice
should only allow set lower price not any price after and during auctionselfRefundsStartTime
check set time only after auction timeWith the exception of issueRefund
, other transfer should allow forward all gas()
to finish the transaction. To prevent case of costing more than 23000 gas for transfer.
It is a good practice to use events for tracking important configurations.
At least add event for mint, bid, summon and most importantly setFinalPrice
daStartTime
, mintListStartTime
, publicStartTime
, claimsStartTime
can be set in setPhaseTimes()
. It is redundant, cost more gas and missing required check to prevent accident by having same atomic function in public.
setDaStartTime
, setMintlistStartTime
, setPublicStartTime
, setClaimsStartTime
does not have same require check as setPhaseTimes()
.
This open up possibility to set wrong configuration that break sale timeline.
It can cause issue when dutch auction end, but owner can restart auction again due to failed auction. Price reaches new minimum when sold out. Already refunded user can get very new refunded price, which can cause confusion when some user now can get refunded again due to lower final price.
setFinalPrice
should only allow set lower price not any price after and during auctionowner
can change final price after dutch auction end to help public sale.
But refunding logic is based on finalPrice
, user might get refunded more than intended if finalPrice
change back to higher amount.
So change final price during self refunding period should not be allowed.
Also for transparency, it is much better for admin can only set lower final price after auction end instead of any price.
selfRefundsStartTime
check set time only after auction timeRefund amount is based on finalPrice
, which can change anytime during dutch auction and by team.
To prevent accident, refund should be required to set way long after dutch action end, even after claimSummon
. To give team enough time to set a sensible finalPrice
amount.
Otherwise, user might get refunded with different price if time overlap with dutch auction.
Help remove bloat function from Minter contract to cut down deployment cost. As these address only needed to change once during constructor.
🌟 Selected for report: BowTiedWardens
Also found by: 0v3rf10w, 0x1f8b, 0x4non, 0xDjango, 0xNazgul, 0xProf, 0xc0ffEE, 0xf15ers, 0xkatana, 0xliumin, ACai, AlleyCat, CertoraInc, Cityscape, Cr4ckM3, DavidGialdi, Dinddle, FSchmoede, Funen, GimelSec, Hawkeye, IllIllI, Kulk0, M0ndoHEHE, MaratCerby, MiloTruck, Picodes, RoiEvenHaim, Tadashi, TerrierLover, TrungOre, VAD37, WatchPug, antonttc, catchup, defsec, delfin454000, dirk_y, eccentricexit, ellahi, fatherOfBlocks, gzeon, hake, hansfriese, hickuphh3, horsefacts, ilan, joestakey, kebabsec, kenta, kenzo, marximimus, minhquanym, noobie, oyc_109, p4st13r4, pauliax, rajatbeladiya, reassor, rfa, robee, rotcivegaf, saian, samruna, shenwilly, shung, simon135, slywaters, sorrynotsorry, throttle, unforgiven, z3s
15.491 USDC - $15.49
TOC
ForgottenRunesWarriorsGuild.sol
mint improvementpublicSummon
duplicate require checkForgottenRunesWarriorsGuild.sol
can remove nonReentrant modifier because Minter contract already have one and only minter can call mint function.
If worry about reentrancy attack, then move line numMinted +=1
to above _safeMint()
function to prevent callback inside ERC721 contract.
ForgottenRunesWarriorsGuild.sol
mint improvementReplace these line with
// save 100 gas from not calling cache number again. uint256 tokenId = numMinted++; // This also prevent any reentrancy attack from minting same token twice _safeMint(recipient, tokenId);
publicSummon
duplicate require checkrequire(numSold < maxForSale, 'Sold out'); require(numSold + numWarriors <= maxForSale, 'Not enough remaining');
Both require check if sale number pass maxForSale. Only 2nd require is necessary. First require only useful for information that user might never see.
SafeTransferLib
instead of current _safeTransferETH()
Improvement based on DisperseApp and SafeTransferLib. New batch refund cut around ~380 gas per address.
Replace issueRefund with:
function issueRefunds(uint256 startIdx, uint256 endIdx) public onlyOwner nonReentrant { require(selfRefundsStarted(), 'Self refund period not started'); //Prevent accident refund during auction as price might change. uint256 end = endIdx + 1; for (uint256 i = startIdx; i < end; i++) { address minter = daMinters[i]; uint256 owed = refundOwed(minter); if (owed > 0) { daAmountRefunded[minter] += owed; safeTransferETH(minter, owed); } } } function safeTransferETH(address to, uint256 amount) internal { bool success; assembly { // I agree that refund call should not forward all gas() // Contract DOS fallback can spend all gas received. success:= call(30000, to, amount, 0, 0, 0, 0) } // save WETH as fallback for griefer in batch call is unnecessary. User can call selfRefund to get WETH as fallback. // Because if the receiver contract do not accept Ether fallback, what will happen if they do not support ERC20 withdrawal as well. The money will be stuck. if(!success) { IWETH(weth).deposit{value: amount}(); IERC20(weth).transfer(to, amount); } }