We audited the thesandboxgame/sandbox-smart-contracts repository at the 1e04f35 commit.
In scope were the following contracts:
packages
└─ asset/
└─ contracts/
├─ Asset.sol
├─ AssetCreate.sol
├─ AuthSuperValidator.sol
├─ interfaces/
│ ├─ IAsset.sol
│ ├─ IAssetCreate.sol
│ ├─ IAssetReveal.sol
│ └─ ITokenUtils.sol
└─ libraries/
└─ TokenIdUtils.sol
The Asset contract implements functionality to allow the minting and burning of Assets (ERC-1155 compliant tokens) in The Sandbox's metaverse.
The users can interact with the AssetCreate contract to create new assets if they have a valid signature from The Sandbox and sufficient Catalyst tokens of the specific tier. This contract is granted the MINTER_ROLE role for the Asset contract and BURNER_ROLE role for the Catalyst contract.
The creation of assets is done by validating the signature, generating a unique token ID, burning the tier-specific catalysts and then minting the asset. Assets can be minted individually or as a batch. This functionality is pausable.
The AssetCreate contract also allows the SPECIAL_MINTER_ROLE privileged role to mint special assets, represented by tier-0.
Within the Asset contract, the minting of assets can only be done by the MINTER_ROLE role (which is the AssetCreate contract). A RoyaltySplitter contract is deployed for each of the minted tokens to ensure that the creator is paid the royalty for their creation. This contract also inherits from the OperatorFiltererUpgradeable to filter out the operators who do not pay royalties to the creators.
Additionally, the Asset contract allows the burning of asset tokens either by the users or by the BURNER_ROLE privileged role.
The AuthSuperValidator contract is used for the verification of signatures and the TokenIdUtils library is used for the generation of asset token IDs.
The audited contracts contain the following privileged roles:
The admin of the Asset contract can perform the following actions:
trustedForwarder.Operator Filterer Registry address.Asset contract to the Operator Filterer Registry.The MINTER_ROLE defined in the Asset contract can mint individual or batches of ERC-1155 asset tokens.
BURNER_ROLE defined in the Asset contract can burn individual or batches of ERC-1155 asset tokens from a specific address.MODERATOR_ROLE defined in the Asset contract can assign a new URI for a valid tokenId.admin of the AssetCreate contract can change the address of the trustedForwarder.PAUSER_ROLE defined in the AssetCreate contract can halt the minting of new assets.SPECIAL_MINTER_ROLE defined in the AssetCreate contract has permission to create special assets like TSB-exclusive tokens.admin of the AuthSuperValidator contract can set signers for the contracts.During the audit, it is assumed that the privileged addresses are trusted entities and would work in the best interest of the platform and its users.
Additionally, it is assumed that only the AssetCreate contract will be assigned the MINTER_ROLE in the Asset contract.
It is also assumed that the BURNER_ROLE in the Catalyst contract would not prevent the asset-minting transactions by front-running the createAsset and createMultipleAssets functions and burning the catalyst tokens for the creator.
DEFAULT_ADMIN_ROLE Can Renounce RoleThe DEFAULT_ADMIN_ROLE role for the AuthSuperValidator contract can be renounced by calling the renounceRole function inherited from the OpenZeppelin contract library's AccessControl contract.
The AuthSuperValidator contract is not upgradable. Once the DEFAULT_ADMIN_ROLE role is renounced, it cannot be recovered, and would result in the inability to set or change signers for the AssetCreate and other contracts that implement similar signature verification.
Consider overriding the renounceRole function to ensure that the DEFAULT_ADMIN_ROLE role is not lost.
Update: Resolved in pull request #1135. The Sandbox team stated:
Added condition to not renounce the
DEFAULT_ADMIN_ROLE.
Several docstrings and inline comments throughout the codebase were found to be erroneous and should be fixed. In particular:
generateTokenId function in the TokenIdUtils contract incorrectly states that chain index is one of the fields used for generating the token ID.setOperatorRegistry function in the Asset contract incorrectly states that the function "sets filter registry address deployed in test". The function will also be used on the mainnets.Update: Resolved in pull request #1136. The Sandbox team stated:
Documentation has been updated.
gap VariablesThroughout the codebase, there are multiple upgradeable contracts that do not have a gap variable. For instance:
Asset contractAssetCreate contractConsider adding a gap variable to avoid future storage clashes in upgradeable contracts.
Update: Resolved in pull request #1137. The Sandbox team stated:
Added
gapvariables.
Within the codebase there are several parts that do not have docstrings.
For instance:
Asset.solAsset.solAsset.solIAsset.solIAsset.solIAsset.solIAsset.solIAsset.solIAsset.solIAssetCreate.solIAssetReveal.solITokenUtils.solITokenUtils.solITokenUtils.solITokenUtils.solITokenUtils.solITokenUtils.solTokenIdUtils.solConsider thoroughly documenting all functions (and their parameters) that are part of any contract's public API. Functions implementing sensitive functionality, even if not public, should be clearly documented as well. When writing docstrings, consider following the Ethereum Natural Specification Format (NatSpec).
Update: Resolved in pull request #1138. The Sandbox team stated:
Function documentation has been updated.
To improve the readability of the code, consider updating the following incomplete docstrings:
getData function in the TokenIdUtils library does not document the return value.mint function in theAsset contract does not document all the input parameters.mintBatch function in theAsset contract does not document all the input parameters.initialize function in theAssetCreate contract does not document all the input parameters.createAsset function in theAssetCreate contract does not document all the input parameters.createMultipleAssets function in theAssetCreate contract does not document all the input parameters._hashMint function in theAssetCreate contract does not document all the input parameters._hashBatchMint function in theAssetCreate contract does not document all the input parameters.Update: Resolved in pull request #1139. The Sandbox team stated:
Documentation has been updated.
Within the codebase, there are instances where the error messages are inconsistent. For example, in the Asset contract, the error message states the contract name followed by the error statement, whereas there is no mention of the contract's name in the error messages of the AssetCreate contract.
Consider adding contract names to all the error messages to know where the error occurred.
Update: Resolved in pull request #1140. The Sandbox team stated:
Added consistent error messages.
Within IAssetReveal.sol, the AssetRevealBurn, AssetRevealBatchBurn, AssetRevealMint and AssetRevealBatchMint events do not have indexed parameters.
Consider indexing event parameters to improve the ability of off-chain services to search and filter for specific events.
Update: Resolved in pull request #1141. The Sandbox team stated:
Added indexed parameters on suggested events .
To improve code readability, consider removing the following typographical errors from the codebase:
Asset.sol, "aditional" should be "additional".Asset.sol, "creactor" should be "creator".Asset.sol, "Opensea.can" should be "Opensea. Can".Update: Resolved in pull request #1142. The Sandbox team stated:
Fixed the suggested typographical errors.
Named return variables are a way to declare variables that are meant to be used within a function's body for the purpose of being returned as the function's output. They are an alternative to explicit in-line return statements.
Throughout the codebase, there are multiple instances of unused named return variables. For instance:
sender return variable in the _msgSender function in Asset.sol.creator return variable in the getCreatorAddress function in Asset.sol.tier return variable in the getTier function in Asset.sol.sender return variable in the _msgSender function in AssetCreate.sol.Consider either using or removing any unused named return variables.
Update: Acknowledged, not resolved. The Sandbox team stated:
We have added named exports but decided to keep the explicit returns. We find that named exports enhance the clarity of what the function returns, so we have chosen to integrate them alongside explicit returns.
In the AssetCreate contract, the createMultipleSpecialAssets function accepts the amounts and metadataHashes parameters and stores them in memory. Given that these arrays are user-defined inputs, their length can be large. calldata is useful for passing large amounts of data to a function without having to copy said data into memory, which can otherwise prove to be expensive in terms of gas usage.
Consider using calldata instead of memory. This will avoid the overhead of copying data into memory, and help reduce the amount of gas needed to execute the function.
Update: Resolved in pull request #1324.
In the Asset contract, the names of the return variables for name and symbol functions start with _. This is inconsistent with how other return variables are named in the contract.
Consider renaming the _name and _symbol variables, adopting a consistent naming convention. This will help improve the consistency and readability of the code.
Update: Acknowledged, not resolved. The Sandbox team stated:
Acknowledged but we have decided not to make any changes in this regard.
In the initialize function of the Asset contract, the baseUri variable is initialized before the forwarder and assetAdmin variables.
Consider initializing the variables in the same order in which they are received as arguments. This will help improve code readability.
Update: Resolved in pull request #1327. The Sandbox team stated:
The order of initialized functions was changed.
Several instances were identified where it would be better to replace the zero-address checks with code existence checks:
Asset.solAsset.solAsset.solAssetCreate.solAsset.solConsider verifying whether a smart contract exists at a given address instead of performing a zero-address check. The isContract function from the OpenZeppelin contracts library's Address contract can be used for this purpose.
Update: Resolved in pull request #1328.
While audits help in identifying code-level issues in the current implementation and potentially the code deployed in production, The Sandbox team is encouraged to consider incorporating monitoring activities in the production environment. Ongoing monitoring of deployed contracts helps identify potential threats and issues affecting production environments. With the goal of providing a complete security assessment, the monitoring recommendations section raises several actions addressing trust assumptions and out-of-scope components that can benefit from on-chain monitoring.
Critical: The AssetCreate and Asset contracts manage what activities can be performed by privileged addresses over asset tokens as well as the burning of catalyst tokens. This contract implements ownership and role-based access controls. Consider monitoring the following events: RoleAdminChanged(bytes32 role, bytes32 previousAdminRole, bytes32 newAdminRole), RoleGranted(bytes32 role, address account, address sender), and RoleRevoked(bytes32 role, address account, address sender).
Critical: The admin for the AuthSuperValidator contract manages the list of addresses that can be signers for a contract. Consider monitoring the following events: RoleAdminChanged(bytes32 role, bytes32 previousAdminRole, bytes32 newAdminRole), RoleGranted(bytes32 role, address account, address sender), and RoleRevoked(bytes32 role, address account, address sender).
Medium: The AssetCreate contract implements an emergency pause mechanism. Consider monitoring for the Paused(address) and Unpaused(address) events.
High: Actions such as minting new assets are previously approved by The Sandbox team by crafting a specific signed message. Under normal circumstances, these transactions are likely going to be successfully executed. Consider monitoring an unusually large number of transactions executing the createAsset or createMultipleAssets functions which revert with the message "Invalid signature", as this could signal attempts to perform signature replay attacks.
The audit was conducted over the course of two weeks. The codebase of the Asset contracts is well-written, making it easy to understand its functionalities and potential vulnerabilities. The Sandbox team was very responsive during the audit, ensuring doubts were addressed in a timely manner. One medium and several low-severity issues were reported, alongside some notes addressing improvement opportunities to the overall quality of the codebase.