PleasrDAO engaged Guardian to review the security of its DN404 tokenization of the coveted “Once Upon A Time In Shaolin” Wu Tang Clan album. From the 20th of May to the 27th of May, a team of 6 auditors reviewed the source code in scope.
- Published
- Review window
- May 20 to 27, 2024
- Language
- Solidity
- Chains
- Ethereum
- Sector
- NFTs
- 0 Critical
- 1 High
- 7 Medium
- 17 Low
- 0 Informational
Scope
Overview
PleasrDAO engaged Guardian to review the security of its DN404 tokenization of the coveted “Once Upon A Time In Shaolin” Wu Tang Clan album. From the 20th of May to the 27th of May, a team of 6 auditors reviewed the source code in scope.
Findings 25
-
H-01 High Missing onlyLive Modifier Access Control Resolved
Description
The
purchaseWithUSDCfunction lacks anonlyLivemodifier, therefore mints can still occur with USDC as a payment token when the contract is disabled.Recommendation
Add an
onlyLivemodifier to thepurchaseWithUSDCfunction.Resolution
PleasrDAO Team: The issue was resolved in commit bb5c069.
-
M-01 Medium NFT Marketplaces Will Not Read Royalty Info Logical Error Resolved
Description
The Token contract implements the ERC2981 standard to signal royalty fees to be taken when NFT sales are made on NFT marketplaces. However the Token contract is the ERC20 compatible contract, not the ERC721 compatible contract, therefore NFT exchanges which will interact with the
DN404Mirrorcontract will not read theroyaltyRecipientandroyaltyFeethat are configured in theTokencontract.As the team currently does not planning on utilizing the royalty feature, the severity of the omission is limited.
Recommendation
Create a contract which inherits
DN404Mirrorand implements the ERC2981 standard with the royalty configurations. Consider adding a function to update the bps for future-proofing.Resolution
PleasrDAO Team: The issue was resolved in commit 63e2585.
-
M-02 Medium contractURI Metadata Is Not Queryable From ERC721 Mirror Logical Error Resolved
Description
The
Tokencontract implements acontractURIfunction which is intended to include collection level metadata about the ERC721 counterpart of the DN404 pair.However, unlike the
tokenURI, thecontractURIcannot be queried from theDN404Mirrorcontract and therefore this metadata is not available for integrators who would query the ERC721 compatible contract for it.Recommendation
Create a contract which inherits from the
DN404Mirrorcontract and adds acontractURIfunction which uses_readStringto query the DN404 base contract, similar to thetokenURIfunction. Then override thedn404Fallbackfunction to add the corresponding selector functionality for a_contractURIfunction.Resolution
PleasrDAO Team: The issue was resolved in commit 7d1d3b7.
-
M-03 Medium Malicious Bid Gas Griefing Griefing Resolved
Description
The DN404 contract has a public
setSkipNFTfunction where themsg.sendercan assign a true or false skip status to themselves.A malicious actor may abuse this functionality to place bids on listed NFTs which would gas grief the lister upon acceptance with the following steps:
- Call
setSkipNFTto set their skip status to true. - Accumulate many ERC20 DN404 tokens by minting through the token contract, but no NFTs since
they have a skip status of true.
- Call
setSkipNFTto set their skip status to false. - Place bids on listed DN404 NFTs, such that if they are accepted the lister would have to expend a
significant amount of gas to mint the NFTs corresponding to the accounts pre-existing ERC20 balance.
This can result in an unexpected loss of funds for the lister through gas expenditure, or even allow for the creation of bids which cannot be accepted as their execution cost would exceed the block gas limit.
Recommendation
Be aware of this risk and document it for users. Consider overriding and disabling the
setSkipNFTfunction to remove this potential griefing vector.Resolution
PleasrDAO Team: The issue was resolved in commit cd3cd28.
- Call
-
M-04 Medium Purchase Price May Significantly Differ Based On The Currency Logical Error Acknowledged
Description
Users can buy Album NFTs with ETH or USDC based on their preference. These token prices are predetermined (0.004 ETH or 15 USDC) and immutable. However, since this is a long term project, there will definitely be significant price movements in terms of ETH/USDC. Users will always choose to buy with the lower price.
None of the users will buy with ETH when the ETH price increases in the long term and the protocol will still get $15 per token in that scenario. However, in the other scenario when ETH price goes down, users will buy with ETH at a much cheaper price. Even if ETH goes to $2500, NFT price per token will be $10 and it is 33% discount in expected sale price.
Recommendation
One option is giving the owner the right to arrange prices based on market movements. The other option is determining a minimum token price in terms of USD value, and charging users at least corresponding amount of ETH if it requires more than 0.004 ETH.
Consider choosing an option based on the protocol’s intentions since the former increases the owner power and the latter requires an oracle implementation and increases complexity.
Resolution
PleasrDAO Team: Acknowledged.
-
M-05 Medium Contract URI Does Not Conform To ERC-7572 Logical Error Partially resolved
Description
In contractURI, the field for external_url should be named external_link instead to conform with ERC-7572 (https://eips.ethereum.org/EIPS/eip-7572). Additionally, ERC-7572 requires an event ContractURIUpdated, which is currently not implemented.
This is also the standard that Opensea uses, see metadata. Not adhering to ERC-7572 could result in improper display of information on secondary marketplaces like Opensea where NFTs are traded. It should be noted however that in tokenURI, external_url is the correct naming.
Recommendation
Rename the media field from
external_urltoexternal_link. Create a separate update function forcontractURIand emit the eventContractURIUpdated.Resolution
PleasrDAO Team: The issue was resolved in commit 1cf921e.
-
M-06 Medium Circumvented Token Transfer Restrictions Validation Resolved
Description
ERC20 tokens are non transferable when the contract is deployed. This is enforced with the
transferableflag, preventing_transferand_transferFromNFTto be executed when thefromaddress is not the zero address, to allow token minting.The DN404 contract does not validate if the
fromaddress is the zero address in thetransferFromfunction. Therefore, users may call the function as follows:transferFrom(address(0), BOB, 0).The call will not revert, the
transferablecondition is circumvented, and aTransferevent will be emitted. Although there is no impact on user's balance, this is an unexpected behavior which may trick off-chain services.Recommendation
Consider overriding the DN404
transferFromfunction to add the zero address check on thefromaddress.Resolution
PleasrDAO Team: The issue was resolved in commit 5b49193.
-
M-07 Medium Zero Amount Purchases Allowed Before And After Minting Period Logical Error Resolved
Description
Both
purchase()andpurchaseWithUSDC()do not validate ifnftAmount_parameters is greater than zero. Even though there is no change in the contract state, there will be unexpected events emitted likeTransfer,IntervalsReducedandMinted.Furthermore, purchases with zero amounts are possible before
startTimeand afterendTimedue to modifiercheckAndUpdateReducedIntervalscalculatingcurrentIntervalsLeftas zero instead of reverting. Again, although there is no impact of contract state, there could be unexpected behavior with off-chain monitoring systems due to this issue.Recommendation
Require that
nftAmount_parameter is non zero in both purchase functions. Also, incheckAndUpdateReducedIntervalsrevert instead of returning zero if before or after minting period.uint256 currentIntervalsLeft = block.timestamp < startTime || block.timestamp >= endTime ? revert BeforeAfterMintingPeriod() : _initialIntervals - ((block.timestamp - startTime) / interval);Resolution
PleasrDAO Team: The issue was resolved in commit 883d02c.
-
L-01 Low Excess ETH Not Refunded Logical Error Acknowledged
Description
Users can purchase NFTs with ETH or USDC, where
purchase()is a payable function in charge of receiving ETH for tokens. It validates if a user has sent enough funds with thecheckPricemodifier. In case a user sends more ETH than the NFT value, these funds will not be refunded, and will be sent to thefundsRecipientinstead.Recommendation
Consider adding a refund function where the excess ETH is first sent to the user and the remaining sent to the funds recipient.
Resolution
PleasrDAO Team: Acknowledged.
-
L-02 Low USDC Token Purchase DoS’ed By Blacklisted fundsRecipient DoS Acknowledged
Description
User purchasing tokens with USDC will call
purchaseWithUSDC(). This function will collect the USDC from the user and transfer it to thefundsRecipient.The USDC token in Base has a blacklist functionality. In case
fundsRecipientaddress gets blacklisted,purchaseWithUSDC()will revert for all users. Due to the fact that there is no way to update thefundsRecipient, users will only be able to buy tokens using ETH.Recommendation
Consider adding an admin function to update the
fundsRecipientaddress.Resolution
PleasrDAO Team: Acknowledged.
-
L-03 Low Invalid secondsReduced Emitted Logical Error Acknowledged
Description
The
checkAndUpdateReducedIntervalsfunction uses round down division to reduce the currentIntervalsLeft by the intervals that have passed. Therefore the final interval in the mint period can be less than 15 minutes, however theMintevent assumes that an entire interval of time was removed from the countdown.Consider the following scenario:
- Intervals are 10 seconds
- The period is 100 seconds in total
- Time is currently at 95 seconds
currentIntervalsLeft = 10 - 95 / 10 = 1- Bob mints the last interval, technically this only removes 5 seconds from the countdown, but the
Mint event emits that it took off the entire 10 seconds
This will misinform consumers of the Mint event and potentially cause issues with integrating off-chain applications.
Recommendation
Consider using round up division to compute the
currentIntervalsLeft, thus not allowing for partial interval mints.Resolution
PleasrDAO Team: Acknowledged.
-
L-04 Low Countdown May Not Reach Zero Logical Error Acknowledged
Description
The
Tokenconstructor validates theMintConfigparams, and initializes the_initialIntervalsimmutable param with the following formula:_initialIntervals = (mintConfig_.endTime-mintConfig_.startTime) / mintConfig_.interval;
This param will be used to calculate the amount of intervals a user can purchase. When
intervalis not a multiple of the difference betweenendTimeandstartTime, the division will round down, and_initialIntervalsvalue will be 1 interval short. Therefore, when a user purchases all intervals available to buy, the countdown will not reach zero.Consider the following scenario:
- startTime: 100, endTime: 200, interval: 17
- _initialIntervals: 5 (200 - 100 / 17)
- user buys all available intervals at t=100
- final countdown = 15 seconds
endTime - intervalsReduced * interval - block.timestamp = 200 - 5*17 - 100 = 15Recommendation
Ensure the difference in time between end and start time is an even multiple of interval. Otherwise, consider using a round up division to make sure countdown reaches zero when all available intervals are purchased. Keep in mind this will cause countdown to be negative in some cases.
Resolution
PleasrDAO Team: Acknowledged.
-
L-05 Low reduceIntervals Lacking Input Validation Validation Resolved
Description
reduceIntervals()is an important admin function used to reduce intervals without minting NFTs. The initial number of intervals is estimated at 2.1 mil (41 mil minutes / 20 min intervals). Given this large number, admin input error is possible when trying to reduce the number of intervals.If an incorrect
intervalAmount_input is used, it could greatly reduce the amount of time left for minting in an irreversible way.Recommendation
Consider some form of input validation, for example by adding a
max interval amountcheck.Resolution
PleasrDAO Team: The issue was resolved in commit 7087a65.
-
L-06 Low tokenURI Fields Not Fully Configurable Logical Error Acknowledged
Description
The specification document states that update of
tokenURImetadata is a mandatory feature. While there is asetMediafunction implemented, it does not allow for alltokenConfigfields to be updated.tokenConfighas a total of 14 fields whilesetMediacan only update 5 of those fields. Fields such asimageandencrypted_media_urlare among those excluded and cannot be updated. Allowing these fields to be updated is important for possible features to be added in the future.Recommendation
Allow
setMediato configure all oftokenConfigfields.Resolution
PleasrDAO Team: Acknowledged.
-
L-07 Low Lack Of Indexed Parameter In Event Events Acknowledged
Description
The
Mintedevent lacks an indexed parameter fortoaddress, so off-chain services will not be able to filter them by user address.Recommendation
Add the indexed
toparameter:event Minted(address indexed to, uint256 amount, uint256 secondsReduced);Resolution
PleasrDAO Team: Acknowledged.
-
L-08 Low Liquidity Pool Considerations Unexpected Behavior Acknowledged
Description
While ERC-721 royalties cannot be enforced, royalties could be earned indirectly through fees from staking in Liquidity Pools (LPs), because of DN404's dual nature as an ERC20.
Recommendation
Therefore, the protocol should consider:
- Setting aside an amount of tokens to create LP
- Adding admin ability to
setSkipNFTfor the liquidity pool contract to prevent it from minting NFTs
each time a swap is done so as to improve gas efficiency.
Resolution
PleasrDAO Team: Acknowledged.
-
L-09 Low Typo Typo Resolved
Description
In the
Tokenconstructor the royalty recipient parameter is misspelled asroyaltRecipient_.Recommendation
Correct the
royaltRecipient_toroyaltyRecipient_.Resolution
PleasrDAO Team: The issue was resolved in commit 63e2585.
-
L-10 Low Unused Custom Error Optimization Resolved
Description
The
InvalidMintcustom error is defined but never used.Recommendation
Remove unused custom error.
Resolution
PleasrDAO Team: The issue was resolved in commit 5b49193.
-
L-11 Low Memory Parameters Can Be Calldata Optimization Resolved
Description
The
string memoryparameters for thesetMediafunction are never mutated and therefore can be declared ascalldata.Recommendation
Convert the
string memoryparameters to stringcalldataparameters.Resolution
PleasrDAO Team: The issue was resolved in commit a8700e5.
-
L-12 Low Variables Can Be Declared Immutable Mutability Resolved
Description
In the
Tokencontract thestartTime,endTime, andintervalstorage variables are assigned to only once in the constructor and are never reassigned, therefore they can be declared asimmutable.Recommendation
Declare the
startTime,endTime, and interval storage variables asimmutable.Resolution
PleasrDAO Team: The issue was resolved in commit e97717d.
-
L-13 Low Mirror Contract Owner Should Be Synced In The Constructor Logical Error Resolved
Description
DN404 and Mirror contracts are synced with
_initializeDN404, and the owner of the DN404 is updated with_initializeOwnerin the constructor. However, this action do not update the owner of the Mirror contract.After the initialization of these contracts, the owner of the Mirror contract is still
address(0)even though the owner of the DN404 is theowner. Anyone can call thepullOwnerfunction and sync them, but there will be a mismatch until this function is called.Recommendation
Call the
pullOwnerfunction in the constructor as a last step after calling_initializeDN404.Resolution
PleasrDAO Team: The issue was resolved in commit 43c8bc5.
-
L-14 Low Inconsistent Media Field Names Best Practices Resolved
Description
Throughout the
Tokencontract snake case is used to represent the names of url media fields, however the animation url field is namedanimationURL, which does not follow the snake case standard for url fields.Recommendation
Consider if the
animationURLfield should be renamed asanimation_url. Additionally, ensure the other fields have the appropriate expected formatting.Resolution
PleasrDAO Team: The issue was resolved in commit c7c9871.
-
L-15 Low Unimplemented Feature Optimization Acknowledged
Description
According to protocol specs, the
purchasefunction “Should try to mint max to, or up to that much if there is less supply left”.However, the function reverts when the requested amount is more than the remaining supply. This might cause a big purchase to revert when there are still tokens to be minted, and the user may lose their chance while trying again with a lower amount due to race condition if the demand is high.
Recommendation
Consider updating the function in a way to mint the remaining supply or document this behaviour.
Resolution
PleasrDAO Team: Acknowledged.
-
L-16 Low NFTMetadataRenderer Library Collection Size Is 0 Optimization Acknowledged
Description
_tokenURIfunction usesNFTMetadataRendererlibrary to create the metadata for NFTs. According to this library, last parameter of thetokenURIMetadatafunction should be the size of entire edition. However, the function is called with the value0and the metadata is not rendered with the true collection size as expected.Recommendation
This may be the expected behavior, however if it is not use the current NFT supply as the size while rendering metadata, with the knowledge that this size can fluctuate.
Resolution
PleasrDAO Team: Acknowledged.
-
L-17 Low Users Can Game The Leaderboard System Gaming Resolved
Description
Users can climb the leaderboard by minting Album tokens and will get incentives based on their leaderboard ranking. The leaderboard is determined solely based on the
mintCount.mintCountis only updated when a user purchases and mints directly but it is not updated after transfers or secondary sales. This is done to prevent gaming like buying a lot of tokens from secondary market just before the sale ends.A user could potentially mint many tokens, sell them all, and use the proceeds to mint more tokens in order to move up the leaderboard while only expending a limited amount of initial capital. This risk is however not applicable if token transfers are disabled during the entire period of the sale.
Recommendation
Be sure to keep token transfers disabled during the entire period of the sale.
Resolution
PleasrDAO Team: Resolved.
No findings match.
Invariants 45
The review's fuzzing suite asserted 45 invariants. 43 held and 2 did not.
Every invariant tested
| ID | Invariant | Result |
|---|---|---|
PD-01 | Sum of Owned NFTs == Mirror Total Supply | Held |
PD-02 | Sum of Owned ERC20 == Token Total Supply | Held |
PD-03 | No User Owns type(uint32).max NFT | Held |
PD-04 | Allowance Matches Approved Amount | Held |
PD-05 | Owner Auxiliary Data Is Not Modified Upon Approval | Held |
PD-06 | Spender Auxiliary Data Is Not Modified Upon Approval | Held |
PD-07 | ERC20 Balance Changes By Amount For Sender And Receiver Upon Transfer | Held |
PD-08 | ERC20 Balance Remains The Same Upon Self-Transfer | Held |
PD-09 | ERC20 Total Supply Remains The Same Upon Transfer | Held |
PD-10 | Auxiliary Data Is Not Modified Upon Transfer | Held |
PD-11 | ERC20 Balance Changes By Amount For Sender And Receiver Upon TransferFrom | Held |
PD-12 | ERC20 Balance Is the Same Upon Self-Transfer Upon TransferFrom | Held |
PD-13 | ERC20 Total Supply Remains The Same Upon TransferFrom | Held |
PD-14 | Auxiliary Data Is Not Modified Upon TransferFrom | Held |
PD-15 | From Address != Address 0 Upon TransferFrom When Not Transferrable | Broken |
PD-16 | From/To Address Should Match Transfer Event | Held |
PD-17 | User Balance Increased By Mint Amount | Held |
PD-18 | Total ERC20 Supply Increased By Mint Amount | Held |
PD-19 | Total NFT Supply Post-Mint Is At Least Total NFT Supply Pre-Mint | Held |
PD-20 | Auxiliary Data Increased By Mint Amount | Held |
PD-21 | Approved NFT Spender == Requested Approval | Held |
PD-22 | Owner Of NFT ID Is Not Modified Upon Approval Of NFT | Held |
PD-23 | Owner Auxiliary Data Is Not Modified Upon Approval | Held |
PD-24 | Spender Auxiliary Data Is Not Modified Upon Approval | Held |
PD-25 | NFT Balance Of Sender and Receiver Accurately Updated Upon TransferNFT | Held |
PD-26 | Sender/Receiver ERC20 Balance Decremented/Incremented By Unit | Held |
PD-27 | NFT Balance Is the Same Upon Self-Transfer Upon TransferNFT | Held |
PD-28 | Receiver Address Is The Owner At The Sent NFT ID | Held |
PD-29 | Total NFT Supply Is Unchanged Upon NFT Transfer | Held |
PD-30 | Approval Is Reset Upon NFT Transfer | Held |
PD-31 | Sender Auxiliary Data Is Not Modified Upon NFT Transfer | Held |
PD-32 | Receiver Auxiliary Data Is Not Modified Upon NFT Transfer | Held |
PD-33 | Skip NFT Status Is Updated To Requested Status | Held |
PD-34 | Auxiliary Data Is Not Modified Upon Set Skip NFT | Held |
PD-35 | Set Approval For All Updated To Requested Status | Held |
PD-36 | Owner Auxiliary Data Is Not Modified Upon Set Approval For All | Held |
PD-37 | Spender Auxiliary Data Is Not Modified Upon Set Approval For All | Held |
PD-38 | _ownerAt(id) Is Always The Same As NFT Holder | Held |
PD-39 | NFT ID Must Be Less Than Or Equal To Total Supply Of NFTs | Held |
PD-40 | Intervals should not be reduced when mint is not live | Broken |
PD-41 | User Mint Count Equals Intervals Reduced Minus Admin Reduced Intervals | Held |
PD-42 | Current Intervals Should Be Greater Than or Equal To Intervals Reduced | Held |
PD-43 | Intervals Reduced Should Increase By Mint Amount | Held |
PD-44 | URI returns Expected Information Upon contractURI | Held |
PD-45 | Token URI Fields Are Updated Accurately Upon setMedia | Held |
Put your code through the same review.
This review started with a conversation about scope. Tell us what you are building and we will plan yours with you.
