Skip to content
$1,000,000 in security audit grants are live now, Apply here →

Security review · June 2024

Shaolin Album Tokenization

for PleasrDAO

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

14 resolved · 1 partially resolved · 10 acknowledged

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

  1. H-01 High Missing onlyLive Modifier Access Control Resolved
    Location
    Token.sol: 173

    Description

    The purchaseWithUSDC function lacks an onlyLive modifier, therefore mints can still occur with USDC as a payment token when the contract is disabled.

    Recommendation

    Add an onlyLive modifier to the purchaseWithUSDC function.

    Resolution

    PleasrDAO Team: The issue was resolved in commit bb5c069.

  2. M-01 Medium NFT Marketplaces Will Not Read Royalty Info Logical Error Resolved
    Location
    Global

    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 DN404Mirror contract will not read the royaltyRecipient and royaltyFee that are configured in the Token contract.

    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 DN404Mirror and 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.

  3. M-02 Medium contractURI Metadata Is Not Queryable From ERC721 Mirror Logical Error Resolved
    Location
    Token.sol: 253

    Description

    The Token contract implements a contractURI function which is intended to include collection level metadata about the ERC721 counterpart of the DN404 pair.

    However, unlike the tokenURI, the contractURI cannot be queried from the DN404Mirror contract 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 DN404Mirror contract and adds a contractURI function which uses _readString to query the DN404 base contract, similar to the tokenURI function. Then override the dn404Fallback function to add the corresponding selector functionality for a _contractURI function.

    Resolution

    PleasrDAO Team: The issue was resolved in commit 7d1d3b7.

  4. M-03 Medium Malicious Bid Gas Griefing Griefing Resolved
    Location
    Global

    Description

    The DN404 contract has a public setSkipNFT function where the msg.sender can 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 setSkipNFT to 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 setSkipNFT to 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 setSkipNFT function to remove this potential griefing vector.

    Resolution

    PleasrDAO Team: The issue was resolved in commit cd3cd28.

  5. M-04 Medium Purchase Price May Significantly Differ Based On The Currency Logical Error Acknowledged
    Location
    Global

    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.

  6. M-05 Medium Contract URI Does Not Conform To ERC-7572 Logical Error Partially resolved
    Location
    Token.sol: 261

    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_url to external_link. Create a separate update function for contractURI and emit the event ContractURIUpdated.

    Resolution

    PleasrDAO Team: The issue was resolved in commit 1cf921e.

  7. M-06 Medium Circumvented Token Transfer Restrictions Validation Resolved
    Location
    Token.sol: 362

    Description

    ERC20 tokens are non transferable when the contract is deployed. This is enforced with the transferable flag, preventing _transfer and _transferFromNFT to be executed when the from address is not the zero address, to allow token minting.

    The DN404 contract does not validate if the from address is the zero address in the transferFrom function. Therefore, users may call the function as follows: transferFrom(address(0), BOB, 0).

    The call will not revert, the transferable condition is circumvented, and a Transfer event 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 transferFrom function to add the zero address check on the from address.

    Resolution

    PleasrDAO Team: The issue was resolved in commit 5b49193.

  8. M-07 Medium Zero Amount Purchases Allowed Before And After Minting Period Logical Error Resolved
    Location
    Token.sol: 158

    Description

    Both purchase() and purchaseWithUSDC() do not validate if nftAmount_ parameters is greater than zero. Even though there is no change in the contract state, there will be unexpected events emitted like Transfer, IntervalsReduced and Minted.

    Furthermore, purchases with zero amounts are possible before startTime and after endTime due to modifier checkAndUpdateReducedIntervals calculating currentIntervalsLeft as 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, in checkAndUpdateReducedIntervals revert 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.

  9. L-01 Low Excess ETH Not Refunded Logical Error Acknowledged
    Location
    Token.sol: 133

    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 the checkPrice modifier. In case a user sends more ETH than the NFT value, these funds will not be refunded, and will be sent to the fundsRecipient instead.

    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.

  10. L-02 Low USDC Token Purchase DoS’ed By Blacklisted fundsRecipient DoS Acknowledged
    Location
    Token.sol: L179

    Description

    User purchasing tokens with USDC will call purchaseWithUSDC(). This function will collect the USDC from the user and transfer it to the fundsRecipient.

    The USDC token in Base has a blacklist functionality. In case fundsRecipient address gets blacklisted, purchaseWithUSDC() will revert for all users. Due to the fact that there is no way to update the fundsRecipient, users will only be able to buy tokens using ETH.

    Recommendation

    Consider adding an admin function to update the fundsRecipient address.

    Resolution

    PleasrDAO Team: Acknowledged.

  11. L-03 Low Invalid secondsReduced Emitted Logical Error Acknowledged
    Location
    Token.sol: 145

    Description

    The checkAndUpdateReducedIntervals function 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 the Mint event 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.

  12. L-04 Low Countdown May Not Reach Zero Logical Error Acknowledged
    Location
    Token.sol: 106

    Description

    The Token constructor validates the MintConfig params, and initializes the _initialIntervals immutable 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 interval is not a multiple of the difference between endTime and startTime, the division will round down, and _initialIntervals value 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 = 15

    Recommendation

    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.

  13. L-05 Low reduceIntervals Lacking Input Validation Validation Resolved
    Location
    Token.sol:: 199

    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 amount check.

    Resolution

    PleasrDAO Team: The issue was resolved in commit 7087a65.

  14. L-06 Low tokenURI Fields Not Fully Configurable Logical Error Acknowledged
    Location
    Token.sol: 219

    Description

    The specification document states that update of tokenURI metadata is a mandatory feature. While there is a setMedia function implemented, it does not allow for all tokenConfig fields to be updated.

    tokenConfig has a total of 14 fields while setMedia can only update 5 of those fields. Fields such as image and encrypted_media_url are 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 setMedia to configure all of tokenConfig fields.

    Resolution

    PleasrDAO Team: Acknowledged.

  15. L-07 Low Lack Of Indexed Parameter In Event Events Acknowledged
    Location
    Token.sol: 17

    Description

    The Minted event lacks an indexed parameter for to address, so off-chain services will not be able to filter them by user address.

    Recommendation

    Add the indexed to parameter:

    event Minted(address indexed to, uint256 amount, uint256 secondsReduced);

    Resolution

    PleasrDAO Team: Acknowledged.

  16. L-08 Low Liquidity Pool Considerations Unexpected Behavior Acknowledged
    Location
    Global

    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:

    1. Setting aside an amount of tokens to create LP
    2. Adding admin ability to setSkipNFT for 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.

  17. L-09 Low Typo Typo Resolved
    Location
    Token.sol: 88

    Description

    In the Token constructor the royalty recipient parameter is misspelled as royaltRecipient_.

    Recommendation

    Correct the royaltRecipient_ to royaltyRecipient_.

    Resolution

    PleasrDAO Team: The issue was resolved in commit 63e2585.

  18. L-10 Low Unused Custom Error Optimization Resolved
    Location
    Token.sol: 33

    Description

    The InvalidMint custom error is defined but never used.

    Recommendation

    Remove unused custom error.

    Resolution

    PleasrDAO Team: The issue was resolved in commit 5b49193.

  19. L-11 Low Memory Parameters Can Be Calldata Optimization Resolved
    Location
    Token.sol: 219

    Description

    The string memory parameters for the setMedia function are never mutated and therefore can be declared as calldata.

    Recommendation

    Convert the string memory parameters to string calldata parameters.

    Resolution

    PleasrDAO Team: The issue was resolved in commit a8700e5.

  20. L-12 Low Variables Can Be Declared Immutable Mutability Resolved
    Location
    Token.sol: 74-76

    Description

    In the Token contract the startTime, endTime, and interval storage variables are assigned to only once in the constructor and are never reassigned, therefore they can be declared as immutable.

    Recommendation

    Declare the startTime, endTime, and interval storage variables as immutable.

    Resolution

    PleasrDAO Team: The issue was resolved in commit e97717d.

  21. L-13 Low Mirror Contract Owner Should Be Synced In The Constructor Logical Error Resolved
    Location
    Token.sol

    Description

    DN404 and Mirror contracts are synced with _initializeDN404, and the owner of the DN404 is updated with _initializeOwner in 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 the owner. Anyone can call the pullOwner function and sync them, but there will be a mismatch until this function is called.

    Recommendation

    Call the pullOwner function in the constructor as a last step after calling _initializeDN404.

    Resolution

    PleasrDAO Team: The issue was resolved in commit 43c8bc5.

  22. L-14 Low Inconsistent Media Field Names Best Practices Resolved
    Location
    Token.sol: 295

    Description

    Throughout the Token contract snake case is used to represent the names of url media fields, however the animation url field is named animationURL, which does not follow the snake case standard for url fields.

    Recommendation

    Consider if the animationURL field should be renamed as animation_url. Additionally, ensure the other fields have the appropriate expected formatting.

    Resolution

    PleasrDAO Team: The issue was resolved in commit c7c9871.

  23. L-15 Low Unimplemented Feature Optimization Acknowledged
    Location
    Token.sol: 159-172

    Description

    According to protocol specs, the purchase function “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.

  24. L-16 Low NFTMetadataRenderer Library Collection Size Is 0 Optimization Acknowledged
    Location
    Token.sol: 290

    Description

    _tokenURI function uses NFTMetadataRenderer library to create the metadata for NFTs. According to this library, last parameter of the tokenURIMetadata function should be the size of entire edition. However, the function is called with the value 0 and 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.

  25. L-17 Low Users Can Game The Leaderboard System Gaming Resolved
    Location
    Global

    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.

    mintCount is 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.

Invariants 45

The review's fuzzing suite asserted 45 invariants. 43 held and 2 did not.

Every invariant tested
IDInvariantResult
PD-01Sum of Owned NFTs == Mirror Total SupplyHeld
PD-02Sum of Owned ERC20 == Token Total SupplyHeld
PD-03No User Owns type(uint32).max NFTHeld
PD-04Allowance Matches Approved AmountHeld
PD-05Owner Auxiliary Data Is Not Modified Upon ApprovalHeld
PD-06Spender Auxiliary Data Is Not Modified Upon ApprovalHeld
PD-07ERC20 Balance Changes By Amount For Sender And Receiver Upon TransferHeld
PD-08ERC20 Balance Remains The Same Upon Self-TransferHeld
PD-09ERC20 Total Supply Remains The Same Upon TransferHeld
PD-10Auxiliary Data Is Not Modified Upon TransferHeld
PD-11ERC20 Balance Changes By Amount For Sender And Receiver Upon TransferFromHeld
PD-12ERC20 Balance Is the Same Upon Self-Transfer Upon TransferFromHeld
PD-13ERC20 Total Supply Remains The Same Upon TransferFromHeld
PD-14Auxiliary Data Is Not Modified Upon TransferFromHeld
PD-15From Address != Address 0 Upon TransferFrom When Not TransferrableBroken
PD-16From/To Address Should Match Transfer EventHeld
PD-17User Balance Increased By Mint AmountHeld
PD-18Total ERC20 Supply Increased By Mint AmountHeld
PD-19Total NFT Supply Post-Mint Is At Least Total NFT Supply Pre-MintHeld
PD-20Auxiliary Data Increased By Mint AmountHeld
PD-21Approved NFT Spender == Requested ApprovalHeld
PD-22Owner Of NFT ID Is Not Modified Upon Approval Of NFTHeld
PD-23Owner Auxiliary Data Is Not Modified Upon ApprovalHeld
PD-24Spender Auxiliary Data Is Not Modified Upon ApprovalHeld
PD-25NFT Balance Of Sender and Receiver Accurately Updated Upon TransferNFTHeld
PD-26Sender/Receiver ERC20 Balance Decremented/Incremented By UnitHeld
PD-27NFT Balance Is the Same Upon Self-Transfer Upon TransferNFTHeld
PD-28Receiver Address Is The Owner At The Sent NFT IDHeld
PD-29Total NFT Supply Is Unchanged Upon NFT TransferHeld
PD-30Approval Is Reset Upon NFT TransferHeld
PD-31Sender Auxiliary Data Is Not Modified Upon NFT TransferHeld
PD-32Receiver Auxiliary Data Is Not Modified Upon NFT TransferHeld
PD-33Skip NFT Status Is Updated To Requested StatusHeld
PD-34Auxiliary Data Is Not Modified Upon Set Skip NFTHeld
PD-35Set Approval For All Updated To Requested StatusHeld
PD-36Owner Auxiliary Data Is Not Modified Upon Set Approval For AllHeld
PD-37Spender Auxiliary Data Is Not Modified Upon Set Approval For AllHeld
PD-38_ownerAt(id) Is Always The Same As NFT HolderHeld
PD-39NFT ID Must Be Less Than Or Equal To Total Supply Of NFTsHeld
PD-40Intervals should not be reduced when mint is not liveBroken
PD-41User Mint Count Equals Intervals Reduced Minus Admin Reduced IntervalsHeld
PD-42Current Intervals Should Be Greater Than or Equal To Intervals ReducedHeld
PD-43Intervals Reduced Should Increase By Mint AmountHeld
PD-44URI returns Expected Information Upon contractURIHeld
PD-45Token URI Fields Are Updated Accurately Upon setMediaHeld

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.

Get a quote