Security Audit
June 15th, 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 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.
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.
Our understanding of the specification was based on the following sources:
The following source code was reviewed during the audit:
30adb10f3f2580b15feeb600a287dec10b7dd81c
92dca09d6e621f74eb2462b8f338a15886875917
We audited the following contracts specificially for the Smart Accounts audit:
Source Code | SHA256 |
---|---|
contracts/smart-wallet/non-upgradeable/Account.sol |
|
contracts/smart-wallet/non-upgradeable/AccountFactory.sol |
|
contracts/smart-wallet/dynamic/DynamicAccount.sol |
|
contracts/smart-wallet/dynamic/DynamicAccountFactory.sol |
|
contracts/smart-wallet/managed/ManagedAccount.sol |
|
contracts/smart-wallet/managed/ManagedAccountFactory.sol |
|
contracts/smart-wallet/utils/AccountCore.sol |
|
contracts/smart-wallet/utils/AccountExtension.sol |
|
contracts/smart-wallet/utils/BaseAccount.sol |
|
contracts/smart-wallet/utils/BaseAccountFactory.sol |
|
contracts/smart-wallet/utils/BaseRouter.sol |
|
We audited the following contracts specificially for the Open Edition ERC-721 audit:
Source Code | SHA256 |
---|---|
contracts/OpenEditionERC721.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.
Click on an issue to jump to it, or scroll down to see them all.
AccountFactory
’s signers registry
ManagedAccountFactory
ManagedAccount
OpenEditionERC721
supportsInterface
implementation
Clones
version
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. |
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
.
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.
AccountFactory
’s signers registry
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:
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
.
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.
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
.
Use PermissionsEnumerable
storage location in AccountCore._setupRole
function.
ManagedAccountFactory
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.
Consider overriding fallback()
and receive()
functions in ManagedAccountFactory
so that the transaction reverts when native tokens are accidentally transferred to the contract.
ManagedAccount
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.
Consider using the “storage struct” patterns for setting the factory
variable.
OpenEditionERC721
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:
claimConditions
is set to ERC20.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
, …)
Consider checking that no native tokens are transferred (msg.value == 0
) when payment currency is set to ERC20.
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.
Consider including the default extension in the BaseRouter’s getter function to have consistent behavior among different implementations of BaseRouter.
supportsInterface
implementation
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.
Consider changing BaseRouter.supportsInterface
to return true for IERC1155Receiver
and IERC721Receiver
interface ids.
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.
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";
EntryPoint.sol contains the following import:
import ".//Exec.sol";
Consider removing one /
from the above import line.
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
The following import is not required and can be removed:
In OpenEditionERC721.sol:
import "./extension/DelayedReveal.sol"
The following comments in OpenEditionERC721
are incorrect:
minterRole
variable declaration:/// @dev Only MINTER_ROLE holders can sign off on `MintRequest`s and lazy mint tokens.
_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.
Clones
version
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.
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.