Security Audit
October 6th, 2023
Version 1.0.0
Presented by 0xMacro
This document includes the results of the security audit for thirdweb's smart contract code as found in the section titled ‘Source Code’. The security audit was performed by the Macro security team from September 11, 2023 to September 22, 2023.
The purpose of this audit is to review the source code of certain thirdweb Solidity contracts, and provide feedback on the design, architecture, and quality of the source code with an emphasis on validating the correctness and security of the software in its entirety.
Disclaimer: While Macro’s review is comprehensive and has surfaced some changes that should be made to the source code, this audit should not solely be relied upon for security, as no single audit is guaranteed to catch all possible bugs.
The following is an aggregation of issues found by the Macro Audit team:
Severity | Count | Acknowledged | Won't Do | Addressed |
---|---|---|---|---|
Medium | 1 | - | - | 1 |
Low | 3 | - | - | 3 |
Code Quality | 12 | - | - | 12 |
Gas Optimization | 1 | - | - | 1 |
thirdweb was quick to respond to these issues.
Our understanding of the specification was based on the following sources:
MarketplaceV3 and BurnToClaimDropERC721 contracts implement role-based access control including the roles listed below. In particular, DEFAULT_ADMIN_ROLE and EXTENSION_ROLE have high privileges such as making upgrades to the contracts. Both roles are assumed to be trusted and to act in a reliable and good manner.
Specifically, the different roles have the following privileges:
DEFAULT_ADMIN_ROLE
: grant and revoke roles; set platform fee info; set contract URI; set royalty engine
EXTENSION_ROLE
: add, replace, or remove extensions
LISTER_ROLE
: create listings and auctions. Only applies when restriction is enabled. By default, restriction is disabled so that everybody can create listings and auctions.
ASSET_ROLE
: Only NFT contracts with ASSET_ROLE can be listed or auctioned, when restriction is enabled. By default, restriction is disabled so that every NFT can be listed or auctioned.
DEFAULT_ADMIN_ROLE
: grant and revoke roles; set platform fee info; set contract URI; set royalty info; set primary sale recipient; set owner; set claim conditions
EXTENSION_ROLE
: add, replace, or remove extensions
MINTER_ROLE
: lazy mint tokens; reveal the URI
TRANSFER_ROLE
: transfers to or from TRANSFER_ROLE
holders are valid, when transfers are restricted.
The following source code was reviewed during the audit:
46e69070978c23b9533edb381e838a5dddf7ed9d
Specifically, we audited the following contracts within this repository:
Source Code | SHA256 |
---|---|
contracts/prebuilts/marketplace/IMarketplace.sol |
|
contracts/prebuilts/marketplace/direct-listings/DirectListingsLogic.sol |
|
contracts/prebuilts/marketplace/direct-listings/DirectListingsStorage.sol |
|
contracts/prebuilts/marketplace/english-auctions/EnglishAuctionsLogic.sol |
|
contracts/prebuilts/marketplace/english-auctions/EnglishAuctionsStorage.sol |
|
contracts/prebuilts/marketplace/entrypoint/MarketplaceV3.sol |
|
contracts/prebuilts/marketplace/offers/OffersLogic.sol |
|
contracts/prebuilts/marketplace/offers/OffersStorage.sol |
|
contracts/prebuilts/unaudited/burn-to-claim-drop/BurnToClaimDropERC721.sol |
|
contracts/prebuilts/unaudited/burn-to-claim-drop/extension/BurnToClaimDrop721Logic.sol |
|
contracts/prebuilts/unaudited/burn-to-claim-drop/extension/BurnToClaimDrop721Storage.sol |
|
Note: This document contains an audit solely of the Solidity contracts listed above. Specifically, the audit pertains only to the contracts themselves, and does not pertain to any other programs or scripts, including deployment scripts.
In addition, for the marketplace contracts, the audit was focused on the changes made since the previous audit of MarketplaceV3 including:
Click on an issue to jump to it, or scroll down to see them all.
PrimarySale
and PlatformFee
recipients
setMaxTotalSupply
BurnToClaimInfo
OPERATOR_ROLE
is not used
natspec
documentation
maxTotalSupply()
VERSION
used
tokenURI
ERC721Holder
and ERC1155Holder
_msgSender
and _msgData
ERC7201
standard
We quantify issues in three parts:
This third part – the severity level – is a summary of how much consideration the client should give to fixing the issue. We assign severity according to the table of guidelines below:
Severity | Description |
---|---|
(C-x) Critical |
We recommend the client must fix the issue, no matter what, because not fixing would mean significant funds/assets WILL be lost. |
(H-x) High |
We recommend the client must address the issue, no matter what, because not fixing would be very bad, or some funds/assets will be lost, or the code’s behavior is against the provided spec. |
(M-x) Medium |
We recommend the client to seriously consider fixing the issue, as the implications of not fixing the issue are severe enough to impact the project significantly, albiet not in an existential manner. |
(L-x) Low |
The risk is small, unlikely, or may not relevant to the project in a meaningful way. Whether or not the project wants to develop a fix is up to the goals and needs of the project. |
(Q-x) Code Quality |
The issue identified does not pose any obvious risk, but fixing could improve overall code quality, on-chain composability, developer ergonomics, or even certain aspects of protocol design. |
(I-x) Informational |
Warnings and things to keep in mind when operating the protocol. No immediate action required. |
(G-x) Gas Optimizations |
The presented optimization suggestion would save an amount of gas significant enough, in our opinion, to be worth the development cost of implementing it. |
EnglishAuctionsLogic
’s bidInAuction
function is marked as payable
to accept native tokens for bids. However, for auctions using ERC20 as currency, when a user unintentionally sends msg.value > 0
when calling bidInAuction
, the transaction will succeed and the native tokens will be locked in the contract.
The tokens will be locked in the contract, unless a permissioned user adds an extension to enable withdrawing these tokens. In cases where extension permissions have been revoked, there would be no way to withdraw these tokens.
Note that the issue also applies to DirectListingsLogic’s
buyFromListing
function.
Remediations to Consider
Consider adding a check to verify that msg.value == 0
when currency is set to ERC20.
MarketplaceV3
supports sending native tokens directly to the contract via the receive()
function. It is understood that this is needed in order to support WETH for listings.
Since there is no restriction on who can call receive()
, native tokens can get locked when users accidentally send them to the contract.
Remediations to Consider
Consider adding a restriction to the receive()
function so that it is callable only from the nativeTokenWrapper
address. An example of restricting the receive()
function to the WETH address can be found in the UniswapRouter contract.
PrimarySale
and PlatformFee
recipients
PrimarySale
and PlatformFee
recipients can be set to 0x0 during initialization via BurntToClaimDropERC721.initialize
or via setPrimarySaleRecipient
and setPlatformFeeInfo
respectively.
If one of those recipients is set to a 0x0 and corresponding fee > 0 this can result in the following behavior:
_collectPriceOnClaim
with “ERC20: transfer to the zero address”Note that this issue applies to all contracts supporting PrimarySale
and PlatformFee
.
Remediations to Consider
Consider adding checks to prevent setting address(0)
for both PrimarySale
and PlatformFee
recipients.
In MarketplaceV3
the ability to add or update extensions to the contract can be done by an account with the EXTENSION_ROLE
. This role can only be granted and revoked by any account with the DEFAULT_ADMIN_ROLE
, since there is no role admin set for the EXTENSION_ROLE
. In the case where a project using these contracts wants to turn off the ability to add/update extensions, they would have to revoke all users with the EXTENSION_ROLE
as well as users with the DEFAULT_ADMIN_ROLE
, since they can grant the EXTENSION_ROLE
to another user at a later time.
Revoking all accounts with the DEFAULT_ADMIN_ROLE
may be undesirable as it also manages other roles like the LISTER_ROLE
and ASSET_ROLE
.
Remediations to Consider
Set the EXTENSION_ROLE
as its own role admin in the initializer and set an initial account with the EXTENSION_ROLE
, this will allow it so the contract can no longer be upgraded when there are no accounts with the EXTENSION_ROLE
.
setMaxTotalSupply
The intention of BurnToClaimDrop721Logic’s
setMaxTotalSupply
function is to set the maximum number of tokens to be minted rather than - as the name implies - setting the maximum number of token supply.
Remediations to Consider
Consider renaming the function setMaxTotalSupply
and the state var maxTotalSupply
to something more appropriate such as setMaxTotalMinted
and maxTotalMinted
.
BurnToClaimInfo
In BurnToClaim.sol, an admin can set BurnToClaimInfo
via setBurnToClaimInfo(..)
. However, both originContractAddress
and currency
parameter can be set to 0x0
as there is no check preventing this. As a result, subsequent calls to burnAndClaim
would revert inside the verifyBurnToClaim
function.
Remediations to Consider
It is recommended to prevent setting invalid config in the first place, thus consider adding != address(0)
checks to the setBurnToClaimInfo
function and remove the check in the verifyBurnToClaim
function.
OPERATOR_ROLE
is not used
In BurnToClaimDropERC721
and BurnToClaimDrop721Logic
, the OPERATOR_ROLE
is defined but not used anywhere in the code.
Remediations to Consider
Consider removing OPERATOR_ROLE
definitions and corresponding _setupRole
logic from above contracts.
natspec
documentation
BurnToClaim.sol
misses proper natspec
documentation for most of the functions. In some of the other contracts such as BurnToClaimDropERC721.sol
, BurnToClaimDrop721Logic.sol
, and BurnToClaimDrop721Storage.sol
, most of the functions take use of the @dev
tag, but they tend to not include @param
and @return
tags.
Remediations to Consider
Add missing natspec
documentation. Follow natspec guidelines to provide proper documentation of the code.
In BurnToClaimDrop721Logic
, there are two ocurrences where named returns are defined but not assigned:
lazyMint
defines a named return variable batchId
, but the value is returned directly instead of assigning it to the variable._msgSender
defines a named return variable sender
, but the value is returned directly instead of assigning it to the variable.Remediations to Consider
Consider either assigning the return value to the defined return variable or remove the variable at all.
maxTotalSupply()
In BurnToClaimDrop721Logic
, the function _checkTokenSupply
retrieves the state var via data storage but could use maxTotalSupply()
instead to improve code readability.
Remediations to Consider
Consider using maxTotalSupply()
to reduce code size and improve readability.
In BurnToClaimDropERC721
, the function isAuthorizedCallToUpgrade
is an internal function but not prefixed with an underscore.
According to Solidity naming conventions - and as applied everywhere else in the code - private and internal functions should be prefixed with an underscore.
Remediations to Consider
Consider renaming the function from isAuthorizedCallToUpgrade
to _isAuthorizedCallToUpgrade
.
VERSION
used
In MarketplaceV3.sol
the constant VERSION
is set to 1
, but version 2
was already used in a previous version of the Marketplace contracts.
Remediations to Consider
Consider increasing the VERSION
to 3
.
tokenURI
BurnToClaimDrop721Logic’s
tokenURI
function returns a valid URI once the tokenId is lazy minted. However, the tokens are technically not minted until they are claimed. Also for burned tokens the function will return a valid URI.
Although above behavior is as designed, it doesn’t fully comply to the ERC721 standard, which says that tokenURI should throw an exception for invalid tokenIds.
Remediations to Consider
Consider adding documentation to make users aware that above behavior slightly deviates from ERC721 standard.
ERC721Holder
and ERC1155Holder
In MarketplaceV3
, the functions onERC721Received
, onERC1155Received
, and onERC1155BatchReceived
are implemented to indicate support of retrieving ERC721 and ERC1155 tokens.
Remediations to Consider
Consider deriving from OpenZeppelin’s ERC721Holder and ERC1155Holder instead to improve readability.
_msgSender
and _msgData
In MarketplaceV3
, _msgSender
and _msgData
are overridden from ERC2771ContextUpgradable
and Permission
and the ident function logic as from ERC2771ContextUpgradable
is reimplemented again.
Remediations to Consider
Consider re-using the logic from ERC2771ContextUpgradable
by calling ERC2771ContextUpgradeable._msgSender()
and ERC2771ContextUpgradeable._msgData()
respectively to improve readability and reduce code size.
ERC7201
standard
Thirdweb uses the “namespaced storage” (aka “storage struct”) pattern for all upgradable contracts. However, those upgradable contracts are currently not adhering to the ERC7201 standard, which standardizes the storage location used for the “namespace”.
According to ERC7201, this is important because:
These storage usage patterns are invisible to the Solidity and Vyper compilers because they are not represented as Solidity state variables. Smart contract tools like static analyzers or blockchain explorers often need to know the storage location of contract data. Standardizing the location for storage layouts will allow these tools to correctly interpret contracts where these design patterns are used.
A great example of adhering to the ERC7201 standard can be seen in the recently released OpenZeppelin’s v5 pre-release contracts, e.g. see Initializable.sol.
Remediations to Consider
Consider changing the upgradable contracts to adhere to the ERC7201 standard.
In IMarketplace.sol
, the struct members of Listing
, Auction
, and Offer
can be reordered to save storage slots.
In particular, when the struct members TokenType
(1 Byte), Status
(1 Byte), and one of the address
members (20 Bytes) are placed next to each other, they only take 1 storage slot.
Consider reordering the Listing
struct from:
struct Listing {
uint256 listingId;
address listingCreator;
address assetContract;
uint256 tokenId;
uint256 quantity;
address currency;
uint256 pricePerToken;
uint128 startTimestamp;
uint128 endTimestamp;
bool reserved;
TokenType tokenType;
Status status;
}
to:
struct Listing {
uint256 listingId;
uint256 tokenId;
uint256 quantity;
uint256 pricePerToken;
uint128 startTimestamp;
uint128 endTimestamp;
address listingCreator;
address assetContract;
address currency;
TokenType tokenType;
Status status;
bool reserved;
}
The same rules apply to the Auction
and Offer
struct.
Macro makes no warranties, either express, implied, statutory, or otherwise, with respect to the services or deliverables provided in this report, and Macro specifically disclaims all implied warranties of merchantability, fitness for a particular purpose, noninfringement and those arising from a course of dealing, usage or trade with respect thereto, and all such warranties are hereby excluded to the fullest extent permitted by law.
Macro will not be liable for any lost profits, business, contracts, revenue, goodwill, production, anticipated savings, loss of data, or costs of procurement of substitute goods or services or for any claim or demand by any other party. In no event will Macro be liable for consequential, incidental, special, indirect, or exemplary damages arising out of this agreement or any work statement, however caused and (to the fullest extent permitted by law) under any theory of liability (including negligence), even if Macro has been advised of the possibility of such damages.
The scope of this report and review is limited to a review of only the code presented by the thirdweb team and only the source code Macro notes as being within the scope of Macro’s review within this report. This report does not include an audit of the deployment scripts used to deploy the Solidity contracts in the repository corresponding to this audit. Specifically, for the avoidance of doubt, this report does not constitute investment advice, is not intended to be relied upon as investment advice, is not an endorsement of this project or team, and it is not a guarantee as to the absolute security of the project. In this report you may through hypertext or other computer links, gain access to websites operated by persons other than Macro. Such hyperlinks are provided for your reference and convenience only, and are the exclusive responsibility of such websites’ owners. You agree that Macro is not responsible for the content or operation of such websites, and that Macro shall have no liability to your or any other person or entity for the use of third party websites. Macro assumes no responsibility for the use of third party software and shall have no liability whatsoever to any person or entity for the accuracy or completeness of any outcome generated by such software.