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: 18/93
Findings: 4
Award: $589.03
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: throttle
Also found by: 0xDjango, BowTiedWardens, WatchPug, defsec, dipp, fatherOfBlocks, gzeon, hake, reassor, shung, unforgiven
The values of daStartTime
and mintlistStartTime
are very important for this contract, as they are used for calculating price
of NFT. In function setPhaseTimes()
owner of contract can set those variables values and there are some checks but those checks are not enough. for example if admin set daStartTime
as 0x
by mistake, the price will be calculated as lowestPrice
and attacker can buy all the tokens with lowest price in the beginning of auction.
This is setPhaseTimes()
code and there is no advanced checks for newDaStartTime
and newMintlistStartTime
values. by setting them with wrong values, users or team can lose funds.
function setPhaseTimes( uint256 newDaStartTime, uint256 newMintlistStartTime, uint256 newPublicStartTime, uint256 newClaimsStartTime ) public onlyOwner { // we put these checks here instead of in the setters themselves // because they're just guardrails of the typical case require( newPublicStartTime >= newMintlistStartTime, 'Set public after mintlist' ); require( newClaimsStartTime >= newPublicStartTime, 'Set claims after public' ); setDaStartTime(newDaStartTime); setMintlistStartTime(newMintlistStartTime); setPublicStartTime(newPublicStartTime); setClaimsStartTime(newClaimsStartTime); }
daStartTime
will be set as newDaStartTime
and contract uses daStartTime
to calculated currentPrice
. so this function should check that newDaStartTime
is bigger than block.timestamp
. admin shouldn't be able to set daStartTime
to a time in the past because if he does that by mistake, attackers can mint tokens for themself with lowest price.
mintlistStartTime
will bet set as newMintlistStartTime
and contract uses mintlistStartTime
to stop auction phase, and use finalPrice
for next phases. if there wasn't enough time between daStartTime
and mintlistStartTime
then the price algorithm could not reach the lower amount possible and contract will start mintlistSummon
phase with wrong finalPrice
(admin coudn't have enough time before mintlistSummon
phase to set price). so contract should check that newMintlistStartTime> newDaStartTime + daPriceCurveLength + 1 Hour
to avoid this issue.
VIM
Add more checks to the function. like this:
require( newDaStartTime >= block.timestamp + 1 Hour, 'Set correct start time' ); require( newMintlistStartTime >= newDaStartTime + daPriceCurveLength + 1 Hour, 'Set correct start time' );
#0 - gzeoneth
2022-06-18T18:41:24Z
Duplicate of #27
🌟 Selected for report: Kulk0
Also found by: 0x1f8b, 0xDjango, BowTiedWardens, Dinddle, broccolirob, defsec, dirk_y, hyh, rajatbeladiya, throttle, unforgiven
Owner will use teamSummon()
to mint tokens for specific recipient, but there is no limit on the number of tokens this functions create, so if by mistake owner sends wrong number of tokens, the effect can not be fixed.
And there is no reentrancy for this function, so if owner mint toke for bad recipient, then function mint()
of ForgottenRunesWarriorsGuild
will make external contract call (because of safeMint()
) to the recipient, and recipient can call both of the contracts function(right now because of the reentrancy mechanism in mint()
attacker can't steal fund)
This is the code of teamSummon()
, as you can see there is no limit based on count
or block.timestamp
, so if owner by mistake call this with big number in the middle of phases, then contract will mint most of the tokens and whole processes and phases that community, expected to happen will fail.
function teamSummon(address recipient, uint256 count) external onlyOwner { require(address(recipient) != address(0), 'address req'); for (uint256 i = 0; i < count; i++) { _mint(recipient); } }
Also this function calls _mint()
which calls warriors.mint(recipient)
and in warriors.mint()
we have:
function mint(address recipient) public override nonReentrant returns (uint256) { require(numMinted < MAX_WARRIORS, 'All warriors have been summoned'); require(_msgSender() == minter, 'Not a minter'); uint256 tokenId = numMinted; _safeMint(recipient, tokenId); numMinted += 1; return tokenId; }
In the line safeMint()
, there is going to be an external contract call to recipient, so if the receiver was bad actor, he can call contract ForgottenRunesWarriorsMinter
functions, even those with nonReentrant
modifier because teamSummon()
has no nonReentrant
modifier. but of course because all of the functions like bidSummon
and mintlistSummon
and publicSummon
calls mint()
(in the ForgottenRunesWarriorsGuild
) and it has nonRentrant
modifier, attacker can't do any harm in minting but if mint()
didn't had this modifier attacker could have made contract to mint more than MAX_WARRIORS
with this steps:
teamSummon()
with recipient
which is contract address and belongs to a bad actor.teamSummon()
will call mint()
in ForgottenRunesWarriorsGuild
for that recipient
.mint()
will make external call to recipient
becasue it's contract address and the code uses _safeMint()
.bidSummon(1)
(or other summon functions based on the phase).bidSummon()
will call mint()
in ForgottenRunesWarriorsGuild
. (let's assume mint()
don't have nonReentrant
).mint()
we don't have check-effect-intract pattern and contract increase numMinted
value after the _safeMint()
, so this numMInted
will be same as before #1 and attacker can bypass numMinted < MAX_WARRIORS,
and after the whole transaction finished numMinted
will be increased by 2
but and could bypass MAX_WARRIORS
.function mint(address recipient) public override nonReentrant returns (uint256) { require(numMinted < MAX_WARRIORS, 'All warriors have been summoned'); require(_msgSender() == minter, 'Not a minter'); uint256 tokenId = numMinted; _safeMint(recipient, tokenId); numMinted += 1; return tokenId; }
VIM
Add nonReentrant
for teamSummon()
and also set limitation condition for count
in that function.
#0 - KenzoAgada
2022-06-06T05:38:31Z
As far as I see reentrancy issue is not valid, as the warden says there's a reentrancy guard on the sensitive functions.
The issue of no limit on teamSummon
is duplicate of #104.
🌟 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.2807 USDC - $30.28
In contract ForgottenRunesWarriorsGuild, function mint() the check-effect-interaction pattern has not been followed. This is the code:
function mint(address recipient) public override nonReentrant returns (uint256) { require(numMinted < MAX_WARRIORS, 'All warriors have been summoned'); require(_msgSender() == minter, 'Not a minter'); uint256 tokenId = numMinted; _safeMint(recipient, tokenId); numMinted += 1; return tokenId; }
numMinted += 1;
should be before than _safeMint(recipient, tokenId)
. becasue _safeMint()
calls external contract, so if in that receiver contract, it tries to use numMinted
value, it will get wrong number.
🌟 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.4498 USDC - $15.45
In ForgottenRunesWarriorsMinter, function bidSummon()
, it only needs to add msg.sender
to daMinters
if msg.sender
is new minter
. so we can add condition to check (daNumMinted[msg.sender] == 0)
for adding to daMinters.push(msg.sender)
and daMinters
list will be lower and issueRefunds()
will use lower gas.
if(daNumMinted[msg.sender] == 0) { daMinters.push(msg.sender); }