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

thirdweb A-12

Security Audit

June 15th, 2023

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 May 2, 2023 to May 15, 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.

Overall Assessment

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

Severity Count Acknowledged Won't Do Addressed
High 1 - - 1
Medium 5 - - 5
Low 2 - - 2
Code Quality 6 - - 6
Gas Optimization 1 - - 1

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 following contracts specificially for the Smart Accounts audit:

Source Code SHA256
contracts/smart-wallet/non-upgradeable/Account.sol

da316fb1470e63c860c2d16bfac3eb40cbea32185e48452570ec974cddc49d4f

contracts/smart-wallet/non-upgradeable/AccountFactory.sol

17c47494082a575cb03558c43865109f6e1f60be68426cbbca327e7eaf100676

contracts/smart-wallet/dynamic/DynamicAccount.sol

171794c06d55dcb93f2fe5823e7919e9e8775a9e5ca3122a798aa47a026e3e11

contracts/smart-wallet/dynamic/DynamicAccountFactory.sol

085c4304da3f37680f130fa67dcc5b08466d7179aca9171368f4ec19bae16705

contracts/smart-wallet/managed/ManagedAccount.sol

6380fd0e2829c9a45c4329406e02bdca64538722c124bbfaee85060510668aa3

contracts/smart-wallet/managed/ManagedAccountFactory.sol

67f0ab5628020eb187211f688bfaa5b35670fb982ceec13c63e0f5cfcd3ee76c

contracts/smart-wallet/utils/AccountCore.sol

c2d02c5f8653736c2c243e5f6545f681822cf3ea576d6915f26b6a7b5e642122

contracts/smart-wallet/utils/AccountExtension.sol

d6ed33b6e12321ac9d4f1dc2bccdad9cd380a55ea222f75bbfe5347ec89b22a6

contracts/smart-wallet/utils/BaseAccount.sol

05841a1d7f05df8113f5e3c2e692121f69c190bdc3c6072c954a9925d44ab6a2

contracts/smart-wallet/utils/BaseAccountFactory.sol

3cc12e6a1ac3812fac71665f943f75c06450599d7ef596e7b6e8c5b0bfc6721e

contracts/smart-wallet/utils/BaseRouter.sol

6118ab29cd54b45fcb51829a4ab27573ce65207836bd911a987ff8d7fe138eaf

We audited the following contracts specificially for the Open Edition ERC-721 audit:

Source Code SHA256
contracts/OpenEditionERC721.sol

6aeec2162a830e9e1fef319ca22fae8cb56871960bb4ac051ab4c78e94f10d6c

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

Any signer can withdraw the smart wallet’s deposit amount

Topic
Incentive Design
Status
Impact
High
Likelihood
Medium

Description

The spec states the following for the DEFAULT_ADMIN_ROLE:

Only a holder of the role can withdraw the smart wallet’s gas deposit from the AA Entrypoint contract.

However, it is possible for every legit signer to withdraw the smart wallet’s deposit amount from the Entrypoint contract by submitting a user operation that calls Entrypoint.withdrawTo directly from the smart wallet.

The behavior applies to all three wallet setups: Account, ManagedAccount, and DynamicAccount.

Remediations to Consider

Consider adding logic to the execute and executeBatch function that prevents certain calls from executing. This could be achieved by implementing a blockList containing certain function selectors that are not allowed to be called via execute and executeBatch functions.

M-1

Any signer can corrupt the AccountFactory’s signers registry

Topic
Protocol Design
Status
Impact
Medium
Likelihood
Medium

Description

BaseAccountFactory implements addSigner and removeSigner functions to track the registered signers of an account. However, any signer can add and remove signers as they wish by submitting a user operation that calls AccountFactory.addSigner or AccountFactory.removeSigner directly from the smart wallet. This can lead to various issues such as:

  • Admin can’t grant a new signer the SIGNER_ROLE, as the signer has been already added by another signer to the factory’s signer registry.
  • AccountFactory wrongly reports registered signers via getSignersOfAccount. Off-chain components and other protocols may rely on this information which can lead to various issues when returning data is not accurate.

The behavior applies to all three wallet setups: Account, ManagedAccount, and DynamicAccount.

Remediations to Consider

Consider a solution similar to the suggested remediation in [H-1], so that addSigner and removeSigner functions can’t be called via a user operation transaction.

M-2

Initial permissions are not enumerable

Topic
Use Cases
Status
Impact
Medium
Likelihood
High

Description

AccountCore._setupRole is called within the initialize functions of DynamicAccount and ManagedAccount. _setupRole uses permissions.storage location for storing the new role:

PermissionsStorage.Data storage data = PermissionsStorage.permissionsStorage();
data._hasRole[role][account] = true;

In contrast, the AccountExtension.sol inherits from PermissionsEnumerable using permissions.enumerable.storage. As a result, roles added in the initialize function of DynamicAccount and ManagedAccount will not show up in the respective PermissionsEnumerable functions like getRoleMember and getRoleMemberCount.

Remediations to Consider

Use PermissionsEnumerable storage location in AccountCore._setupRole function.

M-3

Native tokens can get locked in ManagedAccountFactory

Topic
Use Cases
Status
Impact
High
Likelihood
Medium

Description

ManagedAccountFactory inherits from BaseRouter which implements the fallback() and receive() functions allowing the contract to receive native tokens. This allows to send native tokens directly to the factory contract. However, there is no possibility to transfer out those tokens, keeping them locked in the contract forever.

Remediations to Consider

Consider overriding fallback() and receive() functions in ManagedAccountFactory so that the transaction reverts when native tokens are accidentally transferred to the contract.

M-4

Potential for storage collision in ManagedAccount

Topic
Upgradability
Status
Impact
High
Likelihood
Medium

Description

In ManagedAccount, the state variable factory doesn’t use the “storage struct” pattern.

This causes the potential for storage collisions to occur, since the contract use the proxy pattern and use delegate calls to read and write to storage. Storage collisions can allow data to be set and/or read from that is not intended, causing many potential issues and vulnerabilities.

Note that for a storage collisions to occur, it is required that an extension is added that also doesn’t use the “storage struct” pattern.

Remediations to Consider

Consider using the “storage struct” patterns for setting the factory variable.

M-5

Native token can get locked in OpenEditionERC721

Topic
Use Cases
Status
Impact
High
Likelihood
Low

Description

OpenEditionERC721 implements the payble claim(…) function (via parent contract Drop.sol) that calls _collecPriceOnClaim to transfer payment to feeRecipient and saleRecipient.

The issue of locking native tokens in the contract occurs under the following circumstances:

  1. Payment currency in claimConditions is set to ERC20.
  2. A user calls claim with appropriate amount of ERC20 tokens approved and accidentally also passes native tokens along.

As a result, the claim call succeeds but the native tokens passed along will be locked within the OpenEditionERC721 contract.

Note that this issue also applies to other Thirdweb contracts that inherit from the Drop contract and implement the _collectPriceOnClaim function (such as ERC20Drop, ERC721Drop, …)

Remediations to Consider

Consider checking that no native tokens are transferred (msg.value == 0) when payment currency is set to ERC20.

L-1

BaseRouter’s getter functions don’t include default extension

Topic
Interoperability
Status
Impact
Low
Likelihood
High

Description

The BaseRouter implementation used by DynamicAccount and ManagedAccount don’t consider the default extensions in its getter functions:

  • getAllExtensions
  • getExtension
  • getExtensionImplementation
  • getAllFunctionsOfExtension

This differs from the behavior of dynamic contracts that use the BaseRouter implementation from the preset folder (see here) where default extensions will be returned in all BaseRouter’s getter functions.

Remediations to Consider

Consider including the default extension in the BaseRouter’s getter function to have consistent behavior among different implementations of BaseRouter.

L-2

Incorrect supportsInterface implementation

Topic
Interoperability
Status
Impact
Medium
Likelihood
Low

Description

In BaseRouter.sol, the supportsInterface function is implemented as follows:

function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) {
    return interfaceId == type(IBaseRouter).interfaceId;
}

Whereas the default extension AccountExtension.sol implements its own supportsInterface function to signal that IERC1155Receiver and IERC721Receiver are implemented:

function supportsInterface(bytes4 interfaceId) public view virtual override(ERC1155Receiver) returns (bool) {
    return
        interfaceId == type(IERC1155Receiver).interfaceId ||
        interfaceId == type(IERC721Receiver).interfaceId ||
        super.supportsInterface(interfaceId);
}

However, the supportsInterface implementation in AccountExtension will never be hit as it is already implemented in BaseRouter. As a consequence, when supportsInterface is called for ManagedAccount and DynamicAccount, it wrongly returns false when called with IERC1155Receiver and IERC721Receiver interface ids.

Remediations to Consider

Consider changing BaseRouter.supportsInterface to return true for IERC1155Receiver and IERC721Receiver interface ids.

Q-1

Potentially susceptible to signature malleability

Topic
Best Practices
Status
Quality Impact
Low

A vulnerable version of ECDSA.sol is used that is susceptible to signature malleability. See details here. The current version of ECDSA.sol being used is identical to Openzeppelin’s version 7.0 whereas the vulnerability has been patched in 7.3.

Contracts that are affected by this vulnerability are those that implement replay protection by marking the signature itself as used instead of any nonce or unique id.

Throughout the Thirdweb code base - whenever ECDSA.sol is used - replay protection happens either via checking the nonce or a unique id. Thus, with current usage of ECDSA.sol, there is no immediate security risk imposed.

Consider updating ECDSA.sol to reflect changes in patched Openzeppelin’s version 7.3. This ensures that no vulnerabilities will be introduced by using ECDSA.sol, regardless of the replay protection being used.

Q-2

Unused imports

Topic
Best Practices
Status
Quality Impact
Low

The following imports are not required and can be removed:

  • In DynamicAccount.sol:

    import "../non-upgradeable/Account.sol";
    
  • in DynamicAccountFactory.sol:

    import "../utils/BaseAccount.sol";
    import "../../openzeppelin-presets/proxy/Clones.sol";
    
  • in ManagedAccountFactory.sol:

    import "../utils/BaseAccount.sol"; 
    import "../../openzeppelin-presets/proxy/Clones.sol";
    
Q-3

Cleanup import

Topic
Best Practices
Status
Quality Impact
Low

EntryPoint.sol contains the following import:

import ".//Exec.sol";

Consider removing one / from the above import line.

Q-4

Misleading variable names

Topic
Naming
Status
Quality Impact
Low

BaseAccountFactory.addSigner contains the following lines:

bool isAlreadyAccount = accountsOfSigner[_signer].add(account);
bool isAlreadySigner = signersOfAccount[account].add(_signer);

The variable names isAlreadyAccount and isAlreadySigner are misleading, as EnumberableSet.add function returns true when a new address is added; and false when the address is already in the mapping.

Consider renaming above variables to something more appropriate such as:

  • isNewAccount
  • isNewSigner
Q-5

Unused import

Topic
Best Practices
Status
Quality Impact
Low

The following import is not required and can be removed:

  • In OpenEditionERC721.sol:

    import "./extension/DelayedReveal.sol"
    
Q-6

Inaccurate comment

Topic
Documentation
Status
Quality Impact
High

The following comments in OpenEditionERC721 are incorrect:

  1. For minterRole variable declaration:
/// @dev Only MINTER_ROLE holders can sign off on `MintRequest`s and lazy mint tokens.
  1. For _canSetSharedMetadata function:
/// @dev Returns whether lazy minting can be done in the given execution context.

Lazy mints are mentioned on both of the above comments which are not supported in OpenEditionERC721. Consider changing the comments appropriately.

G-1

Use optimized Clones version

Topic
Best Practices
Status
Gas Savings
Medium

The OpenZeppelin’s Clones.sol contract has been updated to save gas on clone creation (see here). The version referenced by Thirdweb is still using the unoptimized version.

Consider updating Clones.sol to latest version to save gas costs.

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.