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: 1/93
Findings: 4
Award: $2,556.12
🌟 Selected for report: 0
🚀 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/ForgottenRunesWarriorsMinter.sol#L627-L630 https://github.com/code-423n4/2022-05-runes/blob/060b4f82b79c8308fe65674a39a07c44fa586cd3/contracts/ForgottenRunesWarriorsGuild.sol#L173-L176
The function forwardERC20s()
transfers ERC20 tokens out of the contract to the owner. However, it does not properly handle non-standard ERC20 tokens such as USDT which do not return a bool
when the transfer is called.
The issue is that token
is of type IERC20
which expects a bool
to be returned. However USDT does not return any data. Thus, when token.transfer(msg.sender, amount);
attempts to decode the bool
return parameter (even though it's not used it still attempts to decode it) the decoding fails and the transaction reverts.
The impact is that it is impossible to transfer non-standard ERC20 tokens out of the contract.
function forwardERC20s(IERC20 token, uint256 amount) public onlyOwner { require(address(msg.sender) != address(0)); token.transfer(msg.sender, amount); }
Consider using OpenZepplin's SafeERC20 for the transfer calls.
#0 - KenzoAgada
2022-06-06T05:59:34Z
Duplicate of #2.
#1 - gzeoneth
2022-06-18T17:03:42Z
Duplicate of #70
🌟 Selected for report: gzeon
Also found by: AuditsAreUS, BowTiedWardens, Ruhum, pedroais, shenwilly, teddav
861.5328 USDC - $861.53
It is possible for the owner to withdraw all Ether from the protocol including the amount that should be refunded to the Dutch Auction participants.
This poses a centralisation risk and undermines the effectiveness of the Dutch Auction.
The functions withdraw()
and withdrawAll()
allow the owner to withdraw any arbitrary amount of Ether from the contract.
function withdraw(uint256 _amount) public onlyOwner { require(address(vault) != address(0), 'no vault'); require(payable(vault).send(_amount)); } /** * @notice Withdraw all funds to the vault */ function withdrawAll() public payable onlyOwner { require(address(vault) != address(0), 'no vault'); require(payable(vault).send(address(this).balance)); }
Consider preventing the owner from withdrawing amounts that are attributed to the dutch auction.
This can be done by counting the number of warriors sold in the auction in addition to the total amount paid for in the auction stage. The withdrawable amount by the owner will be withdrawable = address(this).balance - numSoldInDa * finalPrice
.
Note withdrawals should only be allowed to be made after the finalPrice
is set and the finalPrice
should not be modifiable after it has been set.
#0 - cryppadotta
2022-05-05T02:36:50Z
it's true but i'm far more worried about the refunds being stuck in the contract than the centralization concerns. this is noted in the readme
#1 - gzeoneth
2022-06-18T18:17:09Z
Duplicate of #187
🌟 Selected for report: pedroais
Also found by: AuditsAreUS, BowTiedWardens, GimelSec, IllIllI, WatchPug, defsec, leastwood
542.7657 USDC - $542.77
It is possible for the owners to modify the variables startPrice
and lowestPrice
during an auction.
Modifying these variables will cause the price mechanics of the curve to change. This will significantly impact the finalPrice
of the auction and also how much each future user will pay during the auction including the users who have already bid.
For example a user wishing to pay a maximum of 1 ETH bids part way through the auction when current price has decreased to 1 ETH. If the auction continues and the price continues to drop the users will be attributed a refund at the end of the auction. However if the owner modifies the lowestPrice
to increase it to above 1 ETH and the auction does not sell out, the user will not be able to receive a refund since the lowestPrice
is now greater than the amount the user paid. Thus, refundOwed()
will overflow on the line 391 return daAmountPaid[minter] - totalCostOfMints - refundsPaidAlready;
cause any refund transaction to revert.
function setStartPrice(uint256 _newPrice) public onlyOwner { startPrice = _newPrice; } /** * @notice set the dutch auction lowest price */ function setLowestPrice(uint256 _newPrice) public onlyOwner { lowestPrice = _newPrice; }
We reocmmend removing the functions setStartPrice()
and setLowestPrice()
such that they can only be called during the constructor.
Alternatively only allow these variables to be set before the auction has started.
#0 - wagmiwiz
2022-05-05T10:14:14Z
We want the ability to change them post construction as we notified people in ou post the start/end prices may change due to change in the price of ETH before the auction.
However not being able to change price after auction has started is a good idea.
#1 - gzeoneth
2022-06-18T18:00:40Z
Duplicate of #38
🌟 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
35.025 USDC - $35.02
The functions pause()
and unpause()
in ForgottenRunesWarriorsMinter.sol
are not used.
Consider removing the functions and also the Pausable
inheritance.
The following functions allow changes from the owner that could significantly distrupt the contract execution.
It is recommended each of the following functions is removed.
setWarriorAddress()
setWethAddress()
setMaxDaSupply()
setMaxForSale()
setMaxClaimFor()
setDaPriceCurveLength()
setDaDropInterval()
The impact of changing any of these functions can cause a DoS attack on the contract. Whether this is deliberate of by misconfiguration it could prevent normal execution of the sale.
Each of the following functions allows modifying the start time of different phases without verifying that any of the phases and valid and non-overlapping.
setDaStartTime()
setMintListStartTime()
setPublicStartTime()
setClaimStartTime()
Consider removing these functions in favour of setPhaseTimes()
which should set the storage variables directly
Furthermore, add the additional requirements to setPhaseTimes()
. First that the times can only be set once. Then that the daStartTime
is at a time greater than or equal to now such that it has not already passed and finally that the mintlist phase occurs after the dutch auctions.
If the mintlist phase occurs before the dutch auction then finalPrice
will still be zero and users will not have to pay for the warriors.
require(daStartTime == (uint256).max) require(newDaStartTime >= now) require(newMintlistStartTime > newDaStartTime + daPriceCurveLength);
mintlistSummon()
Should Require finalPrice
To Be SetIf mintlistSummon()
is called before finalPrice
is set then the users will not need to pay for the warriors.
We recommend adding the requirement that the finalPrice > 0
before this function may be called.
issueRefunds()
or refundAddress()
is called before finalPrice
Is Set Users Get A Full RefundIf owners call issueRefunds()
or refundAddress()
before finalPrice
is set users get a full refund.
We recommend adding the requirement that finalPrice > 0
in each of these functions to prevent over paying bidders.
currentDaPrice()
On lines 284-285 there is a division before a second division which may introduce rounding errors.
uint256 dropPerStep = (startPrice - lowestPrice) / (daPriceCurveLength / daDropInterval);
Consider performing a multiplication before the division. e.g.
uint256 dropPerStep = (startPrice - lowestPrice) * daDropInterval / daPriceCurveLength;