Platform: Code4rena
Start Date: 20/12/2022
Pot Size: $24,500 USDC
Total HM: 7
Participants: 4
Period: 2 days
Judge: GalloDaSballo
Total Solo HM: 4
Id: 198
League: ETH
Rank: 1/4
Findings: 4
Award: $0.00
🌟 Selected for report: 4
🚀 Solo Findings: 3
🌟 Selected for report: Lambda
Data not available
https://github.com/code-423n4/2022-12-forgotten-runiverse/blob/00a247e70de35d7a96d0ce03d66c0206b62e2f65/contracts/RuniverseLandMinter.sol#L358 https://github.com/code-423n4/2022-12-forgotten-runiverse/blob/ea5fce62baeabf3f9d067ad747cee521d7be3a8a/contracts/RuniverseLand.sol#L195 https://github.com/code-423n4/2022-12-forgotten-runiverse/blob/00a247e70de35d7a96d0ce03d66c0206b62e2f65/contracts/RuniverseLandMinter.sol#L513
Some privileged functions are often unavoidable in smart contracts. However, in these contracts, the privileges are (unnecessarily) very extensive and without checks on the smart contract side:
ownerMint
(or ownerMintUsingTokenId
) to mint an arbitrary number of tokens. While these functions should be used for private minting, there is nothing restricting the owner from minting more than 10,924 plots and using the function later on to mint additional plots.setVestingStart
/ setVestingEnd
/ setLastVestingGlobalId
to change the vesting configuration at any point. This can cause already vested tokens to suddenly become unvested (and therefore untransferable). Furthermore, there is no restriction on the parameters, so an owner could for instance set a vesting end that is 100 years in the future.setPlotsAvailablePerSize
. Therefore, a user might think that he buys a rare plot size, but the plot size becomes very common afterwards, destroying the value of his NFT.Therefore, the user currently has to trust the owner that he does not perform any of the previously described actions.
#0 - GalloDaSballo
2022-12-23T19:40:59Z
On previous contests this would have been a pretty good generic Admin Privilege report under which to group all others.
However the C4udit Output puts these findings OOS https://gist.github.com/JustDravee/4b13fe17b1b8aa7e01a2369b92719e38#m-1-centralization-risk-for-trusted-owners
#1 - OpenCoreCH
2022-12-23T20:58:49Z
@GalloDaSballo How will admin privilege reports in combination with the C4udit be handled in the future (not only asking because of this report, curious in general)? The reason I still reported an issue is that these are specific issues that expand on C4udit's output that describe how an admin can cause significant damage to the user (and describe how / why this can be avoided). I think in the past, expanding on C4udit's output was still valid, but of course it is debatable if expanding on it is even possible because the broad description ("need to be trusted to not perform malicious updates or drain funds.") more or less includes all more specific instances.
So is every admin privilege report from now on invalid because of the C4udit output? I personally think that this is not the ideal solution for the centralization report issue, because C4udit's output does not say anything about the trust assumptions. And for me as an investor or user of a protocol, it makes a huge difference if an admin can set the fee within the range of e.g. 0-1% or if he can steal all funds.
#2 - GalloDaSballo
2022-12-23T21:39:02Z
I think we'll figure it out over time, ultimately there's value in highlighting specific inconsistencies (broken invariants, additional risks based on specific settings)
There is also value in flagging Admin Privilege, however, the output of C4udit allow us to weed out the most basic reports.
In judging this I'll talk to at least 2 more Judges to find common ground
#3 - c4-sponsor
2023-01-04T02:09:57Z
msclecram marked the issue as sponsor confirmed
#4 - GalloDaSballo
2023-01-05T12:43:38Z
After judging the more specific Admin Privileges reports:
Will share with other judges to get their perspective
#5 - c4-judge
2023-01-05T12:43:42Z
GalloDaSballo marked the issue as nullified
#6 - GalloDaSballo
2023-01-06T14:19:49Z
After further talking with other Judges, I believe the finding to be valid and of Medium Severity.
While the fact that the admin has certain privileges is OOS.
The fact that:
Is worth nothing
For these reasons am re-opening and assigning a Medium Severity
#7 - c4-judge
2023-01-06T14:19:53Z
GalloDaSballo marked the issue as satisfactory
#8 - msclecram
2023-01-11T04:31:02Z
We updated the code with the next changes: -Vesting assert for starting time -OwnerMint assert to avoid minting more than plots available per size -We removed SetPlotsAvailablePerSize
https://github.com/bisonic-official/plot-contract/commit/ea8abd7faffde4218232e22ba5d8402e37d96878
🌟 Selected for report: Lambda
Data not available
With the function ownerMintUsingTokenId
, it is possible for the owner to mint a token with an arbitrary token ID. However, this can brick the whole contract and cause a situation where no more mints / buys are possible. This happens when a token ID is minted with that function that is later on also generated with ownerGetNextTokenId
. In that case, the call to runiverseLand.mintTokenId
will fail because the function calls _mint
internally, which reverts when the token ID was already minted:
function _mint(address to, uint256 tokenId) internal virtual { require(to != address(0), "ERC721: mint to the zero address"); require(!_exists(tokenId), "ERC721: token already minted"); ... }
Another problem with this function is that the owner can encode an arbitrary plotSize
in this tokenId
(e.g., also the ones with ID > 4 that are defined in IRuniverseLand
, but are not for sale).
Let's say we have plotsMinted[0] = 101
and plotsMinted[1] = plotsMinted[2] = plotsMinted[3] = plotsMinted[4] = plotGlobalOffset = 0
. The owner uses the function to mint the token ID which corresponds to the token encoding [102][102][0]
(112150186059264). He might not even be aware of that because it is not immediately visible in the decimal representation that 112150186059264 corresponds to [102][102][0]
. This mint increases plotsMinted[0]
to 102. When a user now tries to mint for plot size ID 0, the function _mintTokens
calls ownerGetNextTokenId(0)
, which will return [102][102][0]
= 112150186059264. This will cause the mint to fail because this ID was already minted.
Remove the function ownerMintUsingTokenId
or implement checks that the provided token ID wil not collide with a future token ID (by decoding it and checking that the provided globalCounter
/ localCounter
are impossible).
#0 - GalloDaSballo
2022-12-23T19:39:55Z
Specific inconsistent state caused by setters, per discussion with other judges I will flag and judge separately.
May end up grouping under admin privilege but will give it a chance vs a more generic report
#1 - c4-sponsor
2023-01-04T02:08:38Z
msclecram marked the issue as sponsor confirmed
#2 - c4-judge
2023-01-05T09:19:27Z
GalloDaSballo marked the issue as primary issue
#3 - GalloDaSballo
2023-01-05T11:50:10Z
Per similar discussion to #10 and #11, the Warden has shown a way in which the function can cause an inconsistent state, which will cause reverts.
Because the codebase has checks to avoid inconsistent states, but this finding shows a way to sidestep them, I agree with Medium Severity
#4 - c4-judge
2023-01-06T14:30:15Z
GalloDaSballo marked the issue as selected for report
#5 - msclecram
2023-01-11T04:31:56Z
We updated the code with the next changes: -We removed ownerMintUsingTokenId
https://github.com/bisonic-official/plot-contract/commit/ea8abd7faffde4218232e22ba5d8402e37d96878
🌟 Selected for report: Lambda
Data not available
The function setPlotsAvailablePerSize
can be used for two things:
However, in both cases it can introduce errors that can brick parts of the contract. In case 1 where the number of plots is decreased, it is possible that the new number is smaller than plotsMinted
for a specific ID. This will cause an underflow in getAvailableLands
, which subtracts these values:
plotsAvailableBySize[0] = plotsAvailablePerSize[0] - plotsMinted[0]; plotsAvailableBySize[1] = plotsAvailablePerSize[1] - plotsMinted[1]; plotsAvailableBySize[2] = plotsAvailablePerSize[2] - plotsMinted[2]; plotsAvailableBySize[3] = plotsAvailablePerSize[3] - plotsMinted[3]; plotsAvailableBySize[4] = plotsAvailablePerSize[4] - plotsMinted[4];
On the other hand, when the number of available plots per size is increased, the minting can fail. This can happen because the MAX_SUPPLY
in RuniverseLand
is set to 70000, which is also the sum of all plotsAvailablePerSize
entries. When the sum of these entries is increased beyond 70000, some tokens cannot be minted, because the MAX_SUPPLY
is already reached.
plotsAvailablePerSize[0]
is changed by the owner to 54500, all other entries are kept the same. Because more users prefer cheap plots, they buy all 54500 plots of size 8x8 and 15500 plots of size 16x16. However, this means that 70000 tokens are minted and no one can buy the 32x32 or 64x64, leading to a substantial financial loss for the protocol (because larger investors might have bought them later, but can no longer do so).
Enforce that
plotsMinted
#0 - GalloDaSballo
2022-12-23T20:02:20Z
Lack of check to guarantee invariant
#1 - c4-sponsor
2023-01-04T02:08:21Z
msclecram marked the issue as sponsor acknowledged
#2 - GalloDaSballo
2023-01-05T10:56:32Z
Similarly to #10 and #11, the Warden has shown a way for an invariant to be broken based on configuration.
Because certain aspects of the codebase are using the invariants which can be bypassed as shown above, I agree with Medium Severity
#3 - c4-judge
2023-01-06T14:30:24Z
GalloDaSballo marked the issue as selected for report
#4 - msclecram
2023-01-11T04:32:33Z
We updated the code with the next changes: -We removed setPlotsAvailablePerSize
https://github.com/bisonic-official/plot-contract/commit/ea8abd7faffde4218232e22ba5d8402e37d96878
🌟 Selected for report: Dravee
Also found by: Lambda, hansfriese
Data not available
Judge has assessed an item in Issue #3 as M risk. The relevant finding follows:
MAX_SUPPLY of RuniverseLand could be reached before RuniverseLandMinter mints all tokens because of secondary minter RuniverseLand has a MAX_SUPPLY of 70000, which is also the sum of all plotsAvailablePerSize within RuniverseLandMinter. However, because there is also an (arbitrary) secondaryMinter in RuniverseLand, it can happen that this secondary minter mints some tokens. Then, RuniverseLandMinter will not be able to mint / sell some tokens because the limit was already reached.
#0 - c4-judge
2023-01-06T12:33:50Z
GalloDaSballo marked the issue as duplicate of #16
#1 - c4-judge
2023-01-06T12:33:55Z
GalloDaSballo marked the issue as satisfactory
🌟 Selected for report: Lambda
Also found by: Dravee, cccz, hansfriese
Data not available
mintTokenId
uses _mint
instead of _safeMint
RuniverseLand.mintTokenId
uses _mint
instead of _safeMint
to mint NFTs. Therefore, no callback will be performed for smart contracts and it is not checked if a smart contract can handle NFTs. This should not be a huge security problem, because this function is usually called when a user / contract explicitly requests that a token is minted (and pays for it). It would be very stupid (and a user error) to do this from a contract where the NFT is afterwards not retrievable. However, the private sale minting is a bit different because this is done by the owner to a list of given addresses. I do not know how this list is created, but there it could potentially happen that a user accidentally provided a smart contract (e.g., a smart contract wallet) and did not think about this. So you might want to consider using _safeMint
instead.
MAX_SUPPLY
of RuniverseLand
could be reached before RuniverseLandMinter
mints all tokens because of secondary minterRuniverseLand
has a MAX_SUPPLY
of 70000, which is also the sum of all plotsAvailablePerSize
within RuniverseLandMinter
. However, because there is also an (arbitrary) secondaryMinter
in RuniverseLand
, it can happen that this secondary minter mints some tokens. Then, RuniverseLandMinter
will not be able to mint / sell some tokens because the limit was already reached.
RuniverseLand.forwardERC20s
The check address(msg.sender) != address(0)
in forwardERC20s
is not necessary because this function is only callble by the owner and msg.sender
cannot be address(0)
(or it could be in theory, but no one has the private key for address(0)
, or we would have huge problems).
Runiverse.withdrawAll
uses send
instead of call
to send ETHThe function withdrawAll
uses send
for sending ETH. This function only provides a 2300 gas stipend, which might not be sufficient. However, in this context this is not very severe in my opinion because the function is only used for recovery purposes and if the owner is currently a recipient that uses more than 2300 gas in its receive
function, it could be temporarily changed to an EOA, so no funds are lost.
Runiverse.forwardERC20s
does not check the transfer
return valueBecause Runiverse.forwardERC20s
does not check the return value of transfer
, some ERC20 transfers that fail (when the token itself does not revert) will not cause the whole transaction to fail. Because this is only a recovery function, this is not very bad, but for usability / to recognize failed transfers (e.g., in a blockchain explorer), reverting in these cases might still be sensible.
RuniverseLand
The constant string R
in RuniverseLand
is used nowhere and can be removed.
ERC721Vestable._beforeTokenTransfer
Because of the calculation in _beforeTokenTransfer
which first divides and then multiplies, it can happen that all tokens are fully vested 10924 seconds (~3 hours) before the configured end, which may be undesirable. Consider rounding up in the first division.
The functions setGlobalIdOffset
& setLocalIdOffsets
include the check
require(!mintlistStarted(), "Can't change during mint");
However, this means that they are potentially callable during the claimlist phase (namely when the claimlist begins before the mintlist, which is quite probable in my opinion). To ensure consecutive counters (and no accidental collisions), this should not be possible, so consider requiring that both the mintlist and the claimlist has not started.
IRuniverseLand.PlotSize
contains non-mintable sizesThe enum PlotSize
als contains entries for 256x256, ..., 4096x4096 plots, although they are not mintable using these contracts. Consider removing them in order to not confuse developers that built on top of the project and use this interface for the integration.
RuniverseLandMinter.mintlisted
has unnecessary argument _leaf
The argument _leaf
is only used to compare it with the hashed _who
argument. This check does not add any benefit and requires all clients (which will often be some off-chain client for this view function) to also calculate this hash unnecessarily.
RuniverseLand._mintTokens
The first check in _mintTokens
(plotsMinted[uint256(plotSize)] < plotsAvailablePerSize[uint256(plotSize)]
) is unnecessary and could be removed. The second check (plotsMinted[uint256(plotSize)] + numPlots <= plotsAvailablePerSize[uint256(plotSize)]
) implies the first one, so there is no need to perform the first one (except for the different error message, but I do not think that this is worth the gas).
#0 - GalloDaSballo
2022-12-23T19:36:41Z
L
BUMPED Valid Dup of #16
R
L
OOS per the rules, ignoring
R
L
Good catch L
R
R
R
#1 - GalloDaSballo
2023-01-06T12:34:24Z
4L 5R
#2 - c4-judge
2023-01-06T13:36:53Z
GalloDaSballo marked the issue as grade-a
#3 - c4-judge
2023-01-06T13:36:56Z
GalloDaSballo marked the issue as selected for report
#4 - GalloDaSballo
2023-01-06T13:38:27Z
Best Report because of strong impact of findings (Low Impact)