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
gap
variables.
Within the codebase there are several parts that do not have docstrings.
For instance:
Asset.sol
Asset.sol
Asset.sol
IAsset.sol
IAsset.sol
IAsset.sol
IAsset.sol
IAsset.sol
IAsset.sol
IAssetCreate.sol
IAssetReveal.sol
ITokenUtils.sol
ITokenUtils.sol
ITokenUtils.sol
ITokenUtils.sol
ITokenUtils.sol
ITokenUtils.sol
TokenIdUtils.sol
Consider 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.sol
Asset.sol
Asset.sol
AssetCreate.sol
Asset.sol
Consider 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.