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

thirdweb A-3

Security Audit

Aug 19th, 2022

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 June 18, 2022 to June 29, 2022.

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 3 1 1 1
Low 7 2 4 1
Code Quality 5 2 - 3
Informational 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:

Specifically, we audited the following contracts within this repository:

Contract SHA256
contracts/pack/Pack.sol

3dc0c8320b4c58a841c5c09eea8fdf4a4de1f44704f0d193d910e68d3a375568

contracts/extension/ContractMetadata.sol

a50e76e5f1d5faca0d76ecb578d346c999eec0fbe7806e6780bd68f1204fbd9f

contracts/extension/Royalty.sol

56d323ffb5d56d80c98a115dfcef8738e5ff1120c67a940a9b67459d13fac969

contracts/extension/Ownable.sol

fe85fc62427b51d9860b1f79796eccd1469a43bd6679fcc7c7e1a31f57dc176c

contracts/extension/Permissions.sol

053f4dc4bdd53ee4f59f049516c7513fde53cc5a461af4068ff9015036220efa

contracts/extension/PermissionsEnumerable.sol

903504914fd1309f65017775edf99f65e6c334dbeb2fee37cc07d8b39dd6439f

contracts/extension/TokenBundle.sol

b181b2d3ef577b59a798800fa39e6a8df75e1d759eee1ecd5fb36231f0ce0af3

contracts/extension/TokenStore.sol

d04bf4fd423d4765e336a9322bf32afc5458e674824b5d50ae2f90ec450f6bca

contracts/extension/interface/IContractMetadata.sol

f0b7ac93fba3fbb8a71bd76da822cceec0fa86b20418835a228c67d06176eaec

contracts/extension/interface/IOwnable.sol

f4e6814d6fa45c709cbd03de2a2fd46fb86d3156cb934f6feb9acaa692deca72

contracts/extension/interface/IPermissions.sol

333d596baf00c08da55bc1671da3f5df65c4a1d9e8d5639e910d1c23ffb7f980

contracts/extension/interface/IPermissionsEnumerable.sol

5993fac74a2908a778d21786cf0542f32c8c57d05a03321175b630948bf4913e

contracts/extension/interface/IRoyalty.sol

8f39cbdfd7fff348f5f002c2ee87f607811e02312a673781e1cd3281694a9568

contracts/extension/interface/ITokenBundle.sol

fe05e8c4123da579aab2a92efe43b925e81443c870ac05b0f3b99bcaee0321bb

contracts/interfaces/IPack.sol

a3a8f38e49e60615ebf87ae9be98e42553a1b240751c3b49b416bdec75ee9776

contracts/interfaces/IWETH.sol

839869bd411a4e68c9a59d2a0c394a087641eeeadeda4956a255dc3179110cc3

contracts/lib/CurrencyTransferLib.sol

ab7e40d1b333d675e23d9d4a4c70836c508b2e8b890cf1c6f3dc554424d1215d

contracts/lib/TWStrings.sol

d1fa327e26529fe1048a230a9fdaa183da21f9418729b665e0d57f68f136de0b

contracts/openzeppelin-presets/metatx/ERC2771ContextUpgradeable.sol

4ef0ce1601048c10a4b0fdc3247062be8f1a9ca0441c862ddfadc16251a31edb

contracts/openzeppelin-presets/token/ERC20/utils/SafeERC20.sol

569cd0b266ff404aeac1a4266a1535121e47907ef1dcea2d55f4c036a11f758e

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

Reward selection exploits

Topic
On-Chain Randomness
Status
Impact
High
Likelihood
Medium
Forwarder sub-issue has been fixed.

Three sub-issues are listed below. The main cause for the high severity category is sub-issue #3 – namely, the current Forwarder.sol implementation. Consider updating the implementation to disallow contract calls.

For the other problems with randomness, consider adding an optional flag that hooks into Chainlink, for the bigger projects that want secure randomness.

1) A block miner can tweak block.timestamp to either get a good reward, or skip if a good reward is not possible

The RNG logic has three parts:

  • msg.sender – always known
  • blockhash of n-1 – known once n-1 is mined
  • difficulty – derived in nth block

Specifically, difficulty is derived from block.timestamp, of which a block miner has some influence. Here is the formula for block.difficulty (source):

block_diff = parent_diff + parent_diff // 2048 * max(1 - (block_timestamp - parent_timestamp) // 10, -99) + int(2**((block.number // 100000) - 2))

Given the above, a miner can submit their own transaction, and tweak the timestamp for either a better reward, or choose to skip the claim in search of a better reward next time.

If a pack gets good traction, validators (post-merge) or miners (pre-merge) will likely attempt to extract value in this way.

2) Users can use the Flashbots to claim rare rewards

Flashbots bundle allows users to group on-chain transactions such that they execute in an all-or-nothing manner. From Flashbot’s website:

Searchers use Flashbots to submit bundles to block builders for inclusion in blocks. Bundles are one or more transactions that are grouped together and executed in the order they are provided. In addition to the searcher's transaction(s) a bundle can also potentially contain other users' pending transactions from the mempool, and bundles can target specific blocks for inclusion as well.

Advanced users can use this to create a bundle with two transactions:

  1. Claim reward from the pack
  2. Call a contract to determine whether they got a rare reward, and revert if not

Due to the behavior of Flashbots bundles, if the second tx fails, then the first will fail too:

Unlike broadcasting a transaction which lands on-chain even if the transaction fails, troubleshooting Flashbots bundles is considerably more challenging, since any of the following circumstances will prevent your bundle from landing on chain:

  1. Transaction failure (ANY within the bundle)
  2. Incentives (gas price + coinbase transfers) not high enough to offset value of block space
  3. Competitors paying more for same opportunity
  4. Bundle received too late to appear in target block
  5. A validator for target slot not running mev-boost

3) Anyone can bypass the tx.origin == msg.sender requirement via a meta-tx

Thirdweb allows a third party to execute transactions on behalf of a user via meta transactions. Packs.sol also implement this feature:

require(isTrustedForwarder(msg.sender) || _msgSender() == tx.origin, "opener cannot be smart contract");

In a meta transaction, a user only needs to create a signature and submit it to a trusted forwarder, having that forwarder execute the transaction in exchange for not directly paying for its gas cost.

Because a forwarder is a contract, any account (EOA or contract) can execute that signed tx on behalf of its user.

This exposes a vulnerability:

  1. Attacker creates a signature with msg.sender as themselves, and calldata for claiming that reward.
  2. Attacker deploys an exploit contract and calls it with said signature.
  3. Exploit contract calls the forwarder to execute the created signed transaction.
  4. Exploit contract can then choose to revert or not, based on the reward received.T

Since this call is coming from a trusted forwarder, the msg.sender == tx.origin check is skipped, and the require statement passes.

M-1

Lost ERC-20 pack tokens result in permanently locked assets

Topic
Asset Mobility
Status
Wont Do
Impact
Medium
Likelihood
Medium

In Pack.sol, when createPack is called, an ERC-20 token is minted for each pack to open. However, if any one of these tokens are lost (e.g. sent to an invalid address), then the assets in the Pack contract are permanently locked.

The impact of this in practice depends on the value of the assets escrowed.

Consider setting expiration date on the pack, allowing the creator to recover any unclaimed assets after that point.

Response by thirdweb

In case of unclaimed rewards, either the pack creator will have sold the packs, or will have it in their account to open it themselves. So, we'd rather put onus on the pack owner to redeem it/transfer to correct addresses.

So, perhaps we'll keep this feature for a later release.

M-3

Users can claim small amounts of tokens for free

Topic
Input Ranges
Status
Impact
High
Likelihood
Low

In DropERC20.sol’s collectClaimPrice():

uint256 totalPrice = (_quantityToClaim * _pricePerToken) / 1 ether;

Here, totalPrice can be calculated as zero when the product of _quantityToClaim and _pricePerToken is smaller than 1 ether. For example:

  • _quantityToClaim = 1e+12 (0.000001 tokens)
  • _pricePerToken = 1e+5 ($0.10 USDC)
  • totalPrice = 1e+17 (0.1 ether) / 1 ether = 0

This allows a user to claim a small amount of tokens for free (assuming the currency is not NATIVE_TOKEN).

Depending on the perceived value of the dropping ERC-20, an attacker may be motivated to claim this way multiple times in a loop.

Consider requiring totalPrice > 0 when _pricePerToken is greater than zero.

M-4

Publishing is potentially too trusted

Topic
Turst Model
Status
Acknowledged
Impact
Medium
Likelihood
High

In ContractPublisher.sol, anyone can claim to have published any contract. While this allows a user or contract to take a trusted publisher address and see what contracts they claimed to have published, they still need to trust the claims of the publisher.

Additionally, a single contract address can be “claimed” by multiple publishers

Consider a design where a contract must approve, in some way, that a publisher has indeed published it, so that ContractPublisher’s on-chain data is more reliable and trustless.

Response by thirdweb

Not fixing (for now).

The issue pointed out requires a design change in the ContractPublisher contract that must be coordinated throughout the thirdweb Release product.

L-1

Pack opening vulnerable to DoS

Topic
3rd Party Behavior
Status
Wont Do
Impact
Medium
Likelihood
Low

When a user calls openPack(), the Pack contract sends them one or many assets, depending on the configuration of the pack. However, if one asset transfer fails, then the entire transaction fails, potentially preventing pack opening altogether.

Example case: If a pack contained USDC, and the Pack contract is added to the USDC blacklist, then every bundle containing USDC will revert on attempts to open.

Based on the ASSET_ROLE code, thirdweb plans to use an asset allow list at some point. Consider using it on launch, to avoid an attacker creating a valuable bundle of assets mixed with their own malicious asset, in attempt to scam others.

Another option to consider is isolating each _transferToken in a try-catch that emits an Event on throw. This will allow for some record keeping around bad txes. It would also need some sort of collecting from open pack retry feature.

Response by thirdweb

Contract owner will be the end users, and they can toggle ASSET_ROLE as required. Pros & cons will be described in design doc.

L-2

Fee-on-transfer ERC-20 incompatibility

Topic
3rd Party Behavior
Status
Wont Do
Impact
Medium
Likelihood
Low

When a user calls createPack(), the Pack contract transfers assets to itself for escrow. However, if one of these assets is a fee-on-transfer ERC-20 token, then the contract will end up with insufficient assets necessary to support opening all packs.

As mentioned in L-1, thirdweb has an allow list ready to use. Consider using it on launch to avoid this issue, or document that these tokens are not supported, or update Pack.sol to perform balance differences by track asset balances on-chain.

Response by thirdweb

Contract owner will be the end users, and they can toggle ASSET_ROLE as required. Pros & cons will be described in design doc.

L-3

supportsInterface() may prevent ERC721 tokens from transferring to Pack.sol

Topic
Standards Compliance
Status
Impact
Medium
Likelihood
Low

ERC721 contracts that query for ERC721Receiver interface support during transfers will fail to transfer NFTs to Pack.sol, making these NFTs impossible to include in packs.

Note that standard ERC721 implementations (e.g. openzeppelin, solmate contract templates) do not perform this check; and rely only on calling onERC721Received() post-transfer. This gives this issue a low likelihood.

Consider updating supportsInterface() to return true for IERC721Receiver.

L-4

No recovery for accidental transfers

Topic
Mistake Recovery
Status
Wont Do
Impact
Low
Likelihood
Medium

Unfortunately it’s common to see accidental transfers of assets to a protocol’s smart contract. If one sends ERC-20, ERC-721, or ERC-1155 tokens to the Pack contract, there is no way to recover them.

Consider a state variable that is set before/after a call to createBundle() (similar to nonReentrant), so Pack can reject incoming transfers where possible.

Response by thirdweb

Not clear how ERC20 accidental transfers would be rejected.

Any function to recover such tokens might affect existing functionality and token balances in the packs.

L-5

Trusted forwarders can be added, not removed

Topic
Protocol Longevity
Status
Acknowledged
Impact
High
Likelihood
Low

Thirdweb ERC2771Context implementation receives an array of trustedForwarder values on initialization. However, there is no way to remove a trusted forwarder.

Consider adding a function to remove a trusted forwarder, or to set the full list of forwarders, to handle cases where forwarders get lost, expired, or compromised.

Response by thirdweb

Not fixing (for now).

Today, a thirdweb pre-built smart contract deployed by users on the dashboard or via SDKs use the same set of trusted forwarders deployed by thirdweb, or external providers like Biconomy.

The issue pointed out requires a design change across all of thirdweb’s smart contracts; it’s fix will be coordinated across the thirdweb products.

L-6

maxTotalSupply can be updated to < totalSupply()

Topic
Input Ranges
Status
Wont Do
Impact
Low
Likelihood
Medium

In DropERC20.sol, setMaxTotalSupply() allows maxTotalSupply to be set to any number. If the value is set to < totalSupply() all present and future claims will all fail, and a potentially misleading event will be emitted (MaxTotalSupplyUpdated()), potentially causing unnecessary confusion for users. Consider updating setMaxTotalSupply() to require a value >= totalSupply().

Note that if the intent is to halt minting immediately, setting maxTotalSupply to present totalSupply() may suffer from race conditions if additional claims are processed ahead of setMaxTotalSupply() . In this case, consider calling setClaimConditions() with an empty _phases array instead. The latter approach will not revert if additional claims precede its execution.

Response by thirdweb

The purpose here was to achieve pausing of claims by setting maxTotalSupply as less than totalSupply().

The absence of checks is intended to transfer responsibility to contract admin.

L-7

Unpublishing may orphan data

Topic
Data Model
Status
Acknowledged
Impact
Low
Likelihood
Low

The publishContract() function adds an entry into compilerMetadataUriToPublishedMetadataUris which is not accounted for during unpublishContract().

This may leave orphan compilerMetadataUri data, causing it to refer to non-existent publishMetadataUri data. Moreover, getPublishedUriFromCompilerUri() will return publishMetadataUris for contracts that are unpublished.

Response by thirdweb

Not fixing (for now).

The issue pointed out requires a design change in the ContractPublisher contract that must be coordinated throughout the thirdweb Release product.

Q-1

Low Test Coverage

Topic
Testing
Status
Acknowledged
Quality Impact
High

DropERC20.sol and ContractPublisher.sol have very light testing. Consider adding more to more confidently affirm the behavior of the contracts.

Q-2

Missing Events

Topic
Events
Status
Quality Impact
Low
  • DropERC20.sol’s setContractUri() does not emit an event.
  • Pack.sol’s setPublisherProfileUri() does not emit an event.

Consider adding an event for these changes.

Q-3

Inconsistent datatype for platformFeeBps

Topic
Data Model
Status
Acknowledged
Quality Impact
Low

The datatype applied to this value varies throughout the contract between uint256, uint128, uint64 and uint16. Consider normalizing to the datatype applied to its storage variable definition of uint128.

Response by thirdweb

Not fixing (yet).

This fix will be coordinated across the relevant thirdweb smart contracts.

Q-4

Unused import

Topic
Extra Code
Status
Quality Impact
Low

In DropERC20.sol, the import of ERC20PausableUpgradeable.sol is not used.

Q-5

Inaccurate getClaimTimestamp() return value

Topic
Data Consistency
Status
Quality Impact
Medium

In DropERC20.sol’s getClaimTimestamp(), when lastClaimTimestamp is zero, nextValidClaimTimestamp should arguably be 0 to indicate that no delay is required, since no prior purchase has occurred.

If this were to be implemented, a minor gas-savings opportunity can be implemented along with it, as the nextValidClaimTimestamp could be skipped, and line 376 could be simplified to:

require(block.timestamp >= nextValidClaimTimestamp, "cannot claim yet.");
I-1

setClaimConditions() will accept phases that cannot become active

Topic
Data Consistency
Status
Acknowledged
Impact
Informational

setClaimConditions() will update claimCondition without comparing individual _phases[i].startTimestamp values with respect to block.timestamp. As a result, it is possible to supply a set of claim conditions which will never execute.

For example, consider calling setClaimCondition with 4 entries in _phases, with startTimestamp values of: 50, 75, 100, 125. If executed when block.timestamp = 100, the first two phases will never occur.

This may impact an owner’s intended phases, or mask accidental errors in phase formulation.

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.