Reach out for an audit or to learn more about Macro
or Message on Telegram

thirdweb 21

Security Audit

October 7th, 2024

Version 1.0.0

Presented by 0xMacro

Table of Contents

Introduction

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 18, 2024 to September 25, 2024.

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.

Overall Assessment

The following is an aggregation of issues found by the Macro Audit team:

Severity Count Acknowledged Won't Do Addressed
High 3 - - 3
Medium 2 - - 2
Low 1 - - 1
Code Quality 2 - - 2

thirdweb was quick to respond to these issues.

Specification

Our understanding of the specification was based on the following sources:

Source Code

The following source code was reviewed during the audit:

We audited the changes related to PR 163 on the following contracts within the modular-contracts repository:

Contract SHA256
src/Core.sol

c6e79c7016e6270eb38da1a8b02c1ce7be31a94e49ac519f83dcf5699f8776f0

src/callback/BeforeMintCallbackERC1155.sol

690637685184aee1605f04c612905dd40996b9887f15c93d0f4c2c29dd582d74

src/callback/BeforeMintCallbackERC721.sol

30831f3dd22472e689f86ba1a27eef0a2e5a58380329aed63251a0bf08419918

src/callback/BeforeMintWithSignatureCallbackERC1155.sol

f4922acc117b7a73d1fbdc943739559acf86122997f3e84e0b9a6d2b7a857b2b

src/callback/BeforeMintWithSignatureCallbackERC20.sol

cdf312524a2361d6580516eaa05e667de865cb8c57fde8069738591a6375453f

src/callback/BeforeMintWithSignatureCallbackERC721.sol

f407f758eebb58dbfbba6b1f1cd524164bd69925d9f6910b67abba497e56e143

src/callback/BeforeTransferCallbackERC1155.sol

7db96264c40a69608e4599a5c1156e736b038d33ceaab08fe619f140ea1c11d3

src/callback/UpdateMetadataCallbackERC1155.sol

2050b85cdb54cd9b5c4f7b3b15cc3b7950950d33d76c6b1011bfabf7eeb680c8

src/callback/UpdateMetadataCallbackERC721.sol

f6cc58b9573ae5d97c7abb9211033a1156c2ff9ccab537f6e402e576a72276fb

src/callback/UpdateTokenIdERC1155.sol

e0164741c9f6df0224a16ed448d884d302b3808e48718964a8d69bea6f1712f2

src/core/token/ERC1155Base.sol

f35c44157859ac6daa14e3061e36165eb42e4c9bcab1f9c5d168831e8f651d5b

src/core/token/ERC1155Core.sol

a09e1501b554379b7e61be33e64528f5150bd5bdd8fcb3c00d885cf46b73c38f

src/core/token/ERC1155CoreInitializable.sol

345484d6226558fe7b0d8d5bab0cc94fec5d3800c3ce77194630900dd73bbd30

src/core/token/ERC20Base.sol

2916be4f254aece5ee0313df511b11ede1f3be7557113613d2abfdbe82979847

src/core/token/ERC20Core.sol

7bea91cea4854234f8f302677dc2cce9ba2cb5927ea6c6828902aa49f47a4ebd

src/core/token/ERC20CoreInitializable.sol

84d67bc5c33cfeff3aecd1ed9a1cba08d21db36358d9924e7035f3e2be538b82

src/core/token/ERC721Base.sol

cab9df14c4feca238efb1648f475c558414dca1db9df5925ff35b31cfea5d52a

src/core/token/ERC721Core.sol

4d8c90c73db1878d6cd5745f9d376227fd17994be03d77ae0dfadf1ee1a692d9

src/core/token/ERC721CoreInitializable.sol

212c612d0246960ea0c611fa7106c1584331bdffe91a17882a3cc560f5cfd5e7

src/module/token/metadata/BatchMetadataERC1155.sol

cd1b49a0262f20f4977b469d69eb6cba169f79df3b469d54fc697310b2d3975a

src/module/token/metadata/BatchMetadataERC721.sol

6b0c267125da46c86c976a3dc72389528d0fc0ccba00a77d38927bcbb711d8d4

src/module/token/metadata/OpenEditionMetadataERC721.sol

88b5ddd46c4c7d1654aa6d5dc9287e6658055844fe69e5cde62ea148bdc5669e

src/module/token/minting/ClaimableERC1155.sol

1b9a52972e15f1b0eb2f7a378daf7a0b02702176952973966a7a7d6bfe936d1c

src/module/token/minting/ClaimableERC20.sol

c8f420d3b3719a39cadaf773bdaabc5bd51958dd2a3370ba199dc31736ffa030

src/module/token/minting/ClaimableERC721.sol

6de66eb9736b396503eb49e5cb76a7076f5ca3275af1b2e45c0dae7252cf608c

src/module/token/minting/MintableERC1155.sol

eb79d8f5fd4f5b37570ed1b1c779d6f0896ae3c0478b952b5dc7115100d2ec60

src/module/token/minting/MintableERC20.sol

182698707d160cae2e1a699095b4635424fd5eef995c2f06177f4b8289608602

src/module/token/minting/MintableERC721.sol

2f216c68735748bc219ff97d0239d29f91e4a18007768ed2c39c522338739796

src/module/token/tokenId/SequentialTokenIdERC1155.sol

24def5ac023b0da33e5b53e18d8987d99e25ba06511d51b440ce2795468c3527

We audited the changes related to non-standard token decimals handling on the following contracts within the contracts repository:

Contract SHA256
contracts/prebuilts/account/token-paymaster/TokenPaymaster.sol

d85414df4b5fd5c70d0f42a4b759c5dcb81bdda8b65043ff3b695ed4cea6af6c

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.

Issue Descriptions and Recommendations

Click on an issue to jump to it, or scroll down to see them all.

Security Level Reference

We quantify issues in three parts:

  1. The high/medium/low/spec-breaking impact of the issue:
    • How bad things can get (for a vulnerability)
    • The significance of an improvement (for a code quality issue)
    • The amount of gas saved (for a gas optimization)
  2. The high/medium/low likelihood of the issue:
    • How likely is the issue to occur (for a vulnerability)
  3. The overall critical/high/medium/low severity of the issue.

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.

Issue Details

H-1

ClaimableERC20 and MintableERC20 modules incorrectly handle tokens with decimals other than 18

Topic
Protocol Design
Status
Impact
High
Likelihood
Medium

ClaimableERC20 and MintableERC20 modules were recently updated to calculate the mint price by dividing pricePerUnit by 1e18.

function beforeMintWithSignatureERC20(address _to, uint256 _amount, bytes memory _data, address _signer)
    external
    payable
    virtual
    override
    returns (bytes memory)
{
    ...
>>  _distributeMintPrice(msg.sender, _params.currency, (_amount * _params.pricePerUnit) / 1e18);
}

Reference: ClaimableERC20.sol#L216

This approach works correctly if the specified currency uses 18 decimal representation. However, the price calculation becomes inaccurate when dealing with currencies that have a different number of decimals.

Consider a scenario where the specified currency is USDC, which only has 6 decimals. The pricePerUnit will be expressed using the 6-decimal representation, but it's still divided by 1e18. Consequently, the calculated mint price will be significantly lower than intended.

Remediation to Consider

Instead of using a hardcoded 1e18 divisor, consider using the decimals value of the specified currency.

H-2

Claimable modules lead to storage collisions when being updgraded

Topic
Upgradability
Status
Impact
High
Likelihood
Medium

Modules use the “Namespaced Storage Layout” to prevent collisions between different modules as well as between the modules and the core contracts. However, this doesn’t protect against storage collisions between different version upgrades of the same module.

ClaimableERC20/721/1155 were recently updated to include a maxMintPerWallet param in the ClaimCondition struct:

    struct ClaimCondition {
        uint256 availableSupply;       // slot 1
        bytes32 allowlistMerkleRoot;   // slot 2
        uint256 pricePerUnit;          // slot 3
        address currency;              // slot 4
>>      uint256 maxMintPerWallet;      // slot 5
        uint48 startTimestamp;
        uint48 endTimestamp;
        string auxData;
    }

Reference: ClaimableERC20.sol#L75

The ClaimCondition is set in the storage using the following layout:

struct Data {
    // sale config: primary sale recipient, and platform fee recipient + BPS.
    ClaimableERC20.SaleConfig saleConfig;
    // claim condition
    ClaimableERC20.ClaimCondition claimCondition;
    // UID => whether it has been used
    mapping(bytes32 => bool) uidUsed;
    // address => how many tokens have been minted
    mapping(address => uint256) totalMinted;
}

Reference: ClaimableERC20.sol#L21-L30

This change is considered an unsafe storage layout change, since an upgrade would overwrite subsequent parameters. This can lead to severe vulnerabilities such as unintentionally setting wrong values for claim conditions or breaking the module’s functionality at all.

Remediation to Consider

Consider changing the storage layout so that upgrades can be made in a safe way. E.g. only add new params at the end of the storage layout or define a new storage layout for each version that reads from a separate storage slot.

Response by thirdweb

The contracts are not deployed to mainnet/prod yet, so restructuring the storage layout will not lead to storage collisions as of now.

H-3

BatchMetadata modules may apply baseURI to incorrect token ids

Topic
Protocol Design
Impact
High
Likelihood
Medium

ERC721Base and ERC1155Base added support for updateMetadataERC721 and updateMetadataERC1155 callbacks, respectively. This allows for setting the baseURI on the mint and mintWithSignature functions.

The callback and thus the respective BatchMetadata module is called when a baseURI is provided, otherwise the callback is skipped:

function mint(address to, uint256 amount, string calldata baseURI, bytes calldata data) external payable {
    uint256 tokenId = _nextTokenId();
>>  if (bytes(baseURI).length > 0) {
        _updateMetadata(to, tokenId, amount, baseURI);
    }
    _beforeMint(to, tokenId, amount, data);
    _safeMint(to, amount, "");
}

Reference: ERC721Base.sol#L191

This approach allows minting tokens without invoking the BatchMetadata module by leaving the baseURI empty. In such cases, the tokenId in ERC721Base or ERC1155Base increments, while the nextTokenIdRangeStart in the module's storage remains at 0. As a result, when a token is subsequently minted with a provided baseURI, the metadata is incorrectly applied to tokenId = 0 instead of the newly minted tokenId.

For example, imagine 100 NFTs are minted for ERC721Base without a baseURI. Then, 1 NFT is minted with a baseURI of ipfs://base/, intended to apply to tokenId = 100. However, this doesn't happen as expected. Because the module wasn't informed of the 100 previously minted NFTs and the nextTokenIdRangeStart is still 0, it mistakenly applies the provided baseURI to tokenId = 0.

Remediation to Consider

Consider changing the logic in the BatchMetadata modules, so that the metadata is correctly applied to the provided token range.

M-1

The SequentialTokenIdERC1155 module fails to apply the correct tokenId when installed after initial minting

Topic
Protocol Design
Status
Impact
High
Likelihood
Low

SequentialTokenIdERC1155 increments the tokenId when a value of uint.max is passed to updateTokenIdERC1155. This works well when the module is installed before minting the first token, but can lead to unwanted behavior if installed at a later point.

Consider a scenario where 10 tokens (range 0-9) have been already minted by ERC1155Core, and only then the SequentialTokenIdERC1155 is installed. The nextTokenId in SequentialTokenIdERC1155 would start from id 0 (instead of 10), which is not the desired behavior. Instead, it should increment the value from the most recent tokenId.

Remediation to Consider

Add a onInstall function to the module to allow setting the nextTokenId accordingly.

M-2

Paymaster is vulnerable to a sandwich attack when refilling the deposit on EntryPoint

Topic
Frontrunning
Status
Impact
Medium
Likelihood
Medium

In TokenPaymaster’s _postOp function, refillEntryPointDeposit is called to refill the deposit on EntryPoint with the Paymaster’s token balance. If the EntryPoint balance falls below the minEntryPointBalance threshold, tokens are swapped to wei using Uniswap:

if (currentEntryPointBalance < tokenPaymasterConfig.minEntryPointBalance) {
    uint256 swappedWeth = _maybeSwapTokenToWeth(token, _cachedPrice);
    unwrapWeth(swappedWeth);
    entryPoint.depositTo{value: address(this).balance}(address(this));
}

Reference: TokenPaymaster.sol#L178-L182

In _maybeSwapTokenToWeth, the amountOutMin is calculated to provide slippage protection for the Uniswap swap operation.

uint256 amountOutMin = addSlippage(tokenToWei(tokenBalance, quote), uniswapHelperConfig.slippage);

Reference: UniswapHelper.sol#L55

The TokenPaymaster’s implementation has been recently changed to support tokens with decimals < 18. However, this is not considered in the calculation of the amountOutMin value. Consequently, the amountOutMin calculated might be significant lower than what is required for sufficient slippage protection.

Let’s consider a scenario where USDC is used (decimal: 6) for the payment token. In this case, the calculated amountOutMint would be by a magnitude of 10^12 (10^18-6) smaller than what is required, effectively removing slippage protection.

This makes the swap operation vulnerable to a sandwich attack, meaning that the Paymaster potentially receives much less WETH for the amount of tokens provided.

Remediation to Consider

Consider the token’s decimal value when calculating the amountOutMin value.

L-1

TokenPaymaster doesn’t support tokens with decimal > 18

Topic
Protocol Design
Status
Impact
Medium
Likelihood
Low

The TokenPaymaster contract was modified to accommodate tokens with decimals other than 18:

uint256 tokenAmount = weiToToken(preChargeNative, cachedPriceWithMarkup) / (10 ** (18 - _tokenDecimals));

Reference: TokenPaymaster.sol#L131

While this implementation works for tokens with decimals ≤ 18, it fails for tokens with decimals > 18. In such cases, the transaction would revert, rendering the Paymaster contract non-functional.

Remediation to Consider

  • Modify the implementation to correctly handle tokens with decimals > 18, or
  • add a check in the constructor to revert if a token with decimals > 18 is provided
Q-1

FallbackFunction array of Claimable modules can be reduced

Topic
Best Practice
Status
Quality Impact
Low

For ClaimableERC20/721/1155, the FallbackFunction array is defined with a size of 5, but only needs to hold 4 elements. Consider reducing the size of the array to 4.

Q-2

Nitpicks

Topic
Nitpicks
Status
Quality Impact
Low

Natspec is missing or incorrect on the following occurrences:

  • missing natspec for _startTokenId param here and here
  • missing natspec for _id param here
  • missing natspec for deadline, v, r, s params here
  • outdated natspec for ClaimSignatureParamsERC20 here
  • outdated natspec for ClaimParamsERC20 here
  • incorrect natspec for beforeMintWithSignatureERC20 here. It misses natspec for all the params and incorrectly says “for the ERC20Core.mint” instead of “ERC20Core.mintWithSignature
  • incorrect natspec here. It mentions “platform fee recipient + BPS”, but this is not used anymore.
  • obsolete natspec for recipient here.

Disclaimer

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.