As of 3 Nov 2026, The Sandbox has 8 audit reports by 2 security firms on Trustblock; the latest is by OpenZeppelin, dated 9 Aug 2023. Those reports list no open critical findings.
Record last updated . It holds audits, findings and contracts up to Nov 2026, and incidents up to Oct 2026. Trustblock does not review code.
Newest first
| Audit | Security firm | Date | Scope | Critical findings | Report |
|---|---|---|---|---|---|
| Land and Tunnel Audit | Security firm: OpenZeppelin | Date: | Scope: Files named in the report | No open critical findings | Open · blog.openzeppelin.com |
| Sandbox L2 | Security firm: CertiK | Date: | Scope: Files named in the report | No open critical findings | Open · skynet.certik.com |
| Sandbox L1 | Security firm: CertiK | Date: | Scope: Files named in the report | No open critical findings | Open · skynet.certik.com |
| Sandbox Sand Reward Pool | Security firm: CertiK | Date: | Scope: Files named in the report | No open critical findings | Open · skynet.certik.com |
| Sandbox PR | Security firm: CertiK | Date: | Scope: Files named in the report | No open critical findings | Open · skynet.certik.com |
| Sandbox V1 | Security firm: CertiK | Date: | Scope: Files named in the report | No open critical findings | Open · skynet.certik.com |
| SandBox V4 | Security firm: CertiK | Date: | Scope: Files named in the report | No open critical findings | Open · skynet.certik.com |
| Sandbox V3 | Security firm: CertiK | Date: | Scope: Files named in the report | 1 critical finding, fixed | Open · skynet.certik.com |
Statuses are the security firm’s own, from each report.
| Severity | Open | Acknowledged | Fixed |
|---|---|---|---|
| Critical | 0 | 0 | 1 |
| High | 1 | 2 | 4 |
| Medium | 1 | 0 | 4 |
| Low | 36 | 1 | 50 |
In the contract `PolygonLandV1`, the role `_admin` has authority over the following functions: - `setPolygonLandTunnel()`: Modify the `PolygonLandTunnel` address; - `setTrustedForwarder()`: Modify the Trusted Forwarder for the Meta Transactions. In addition, the role `polygonLandTunnel` has authority over the following function: - `mint()`: mints quads to an address. The contract `PolygonLandV1` inherits the contract `PolygonLandBaseToken`, where the role `superOperator` has authority over the following functions: - `batchTransferQuad()`: Transfer any user's quads to an address; - `transferQuad()`: Transfer any user's quads to an address. The contract `PolygonLandBaseToken` inherits the contract `ERC721BaseToken`, where `_admin` has authority over the following functions: - `setSuperOperator()`: Give or remove the `superOperator` role to or from an address; - `changeAdmin()`: Change the address of the role `_admin`. In addition, the `superOperator` role has authority over the following functions: - `approve()`: Decide the allowance of any token; - `approveFor()`: Decide the allowance of any token; - `transferFrom()`: Transfer any user's tokens to an address; - `safeTransferFrom()`: Transfer any user's tokens to an address; - `batchTransferFrom()`: Transfer several of a user's tokens to an address; - `safeBatchTransferFrom()`: Transfer several of a user's tokens to an address; - `setApprovalForAllFor()`: Set the approval for an address to manage all of a user's tokens; - `burnFrom()`: Burn any user's tokens. In the contract `LandTunnel`, the role `_owner` has authority over the following functions: - `setTrustedForwarder()`: Modify the Trusted Forwarder for the Meta Transactions; - `pause()`: Pause the contract to disable transfers from L1 (Ethereum) to L2 (Polygon); - `unpause()`: Unpause the contract, reenabling transfers from L1 (Ethereum) to L2 (Polygon). In the contract `PolygonLandTunnel`, the role `_owner` has authority over the following functions: - `setMaxLimitOnL1()`: Define the maximum amount of gas to spend for `batchTransferQuadToL1()` function; - `setMaxAllowedQuads()`: Define the maximum amount of quads to transfer for `batchTransferQuadToL1()` function; - `setLimit()`: Define the maximum amount of gas to spend on transfers for one quad type; - `setupLimits()`: Define the maximum amount of gas to spend on transfers for each quad type; - `setTrustedForwarder()`: Modify the Trusted Forwarder for the Meta Transactions; - `pause()`: Pause the contract, preventing transfers from L2 (Polygon) to L1 (Ethereum); - `unpause()`: Unpause the contract, reenabling transfers from L2 (Polygon) to L1 (Ethereum). Any compromise to the aforementioned privileged accounts may allow a hacker to take advantage of this authority and manipulate the reward system.
Open the reportThe function “_computeValue” is meant to calculate a random value for assigning the value per gem ID. The result of its computation is stored in a variable named “randomValue”.
Open the reportThe report gives no description here.
Open the reportSandbox has implemented Land token protocols on both L1 (Ethereum) and L2 (Polygon). The burning logic of the two protocols is different.\ On L2, the `_burn()` function sets the 160st bit of the token's owner record to be 1 (as an identification for burnt tokens) yet maintains the previous owner's address. ```solidity= //Burning Logic on L2 function _burn( address from, address owner, uint256 id ) internal { require(from == owner, "NOT_OWNER"); uint256 storageId = _storageId(id); _owners[storageId] = (_owners[storageId] & NOT_OPERATOR_FLAG) | BURNED_FLAG; // record as non owner but keep track of last owner _numNFTPerAddress[from]--; emit Transfer(from, address(0), id); } ``` On L1, the `_burn()` function sets the token's owner record as 2**160 and removes the previous owner's address. ```solidity= //Burning Logic on L1 function _burn(address from, address owner, uint256 id) internal { require(from == owner, "not owner"); _owners[id] = 2**160; // cannot mint it again _numNFTPerAddress[from]--; emit Transfer(from, address(0), id); } ``` If tokens on L1 and L2 are designed to be equal, it is recommended to maintain the same logic to avoid unexpected consequences resulting from protocol incompatibility.
Open the reportWhen the contract tries to mint a quad via `_mintQuad()`, there is a check to ensure that no quads containing the quad to mint and no quads (or LANDS) within the quad to mint have already been minted by calling the function `exists()`. This leads to a possible denial-of-service attack where the attacker mints 1x1 LANDS at specific locations to prevent the minting of larger quads. For example, out of the possible 166,464 LAND placements, only 289 LANDS need to be minted to prevent 24x24 quads from occurring. **Proof of Concept**\ The following test shows that if 289 1x1 Land is minted, no one can mint 24x24 land anymore. ```typescript it('Mint 289 land to prevent others mint 24x24', async function () { const {landOwners} = await setupTest(); const bytes = '0x3333'; let totalMint = 0; for (let x = 0; x < GRID_SIZE; x = x + 24) { for (let y = 0; y < GRID_SIZE; y = y + 24 ){ await waitFor( landOwners[0].MockLandWithMint.mintQuad( landOwners[0].address, 1, x, y, bytes )); //console.log('Minted a land on (%d,%d)', x , y) totalMint = totalMint + 1; } } console.log('Step1: Mint lands in each 24x24 land. In total %d 1x1 land is minted', totalMint); let totalTry = 0; for (let x = 0; x < GRID_SIZE; x = x + 24) { for (let y = 0; y < GRID_SIZE; y = y + 24) { await expect( landOwners[0].MockLandWithMint.mintQuad( landOwners[0].address, 24, x, y, bytes ) ).to.be.revertedWith('Already minted'); totalTry = totalTry + 1; } } console.log('Step2: Cannot Mint 24x24 land Anymore. In total %d times tried to mint 24x24 land', totalTry) }); ``` The test case passed, meaning 24x24 land can no longer be minted ```typescript Step1: Mint lands in each 24x24 land. In total 289 1x1 land is minted Step2: Cannot Mint 24x24 land Anymore. In total 289 times tried to mint 24x24 land ✓ Mint 289 land to prevent others mint 24x24 (18107ms) ```
Open the reportThe contract is serving as the underlying entity to interact with third-party **fx-portal** protocols. The **fx-portal** protocol performs the cross-chain functionality to exchange messages between Ethereum and Polygon. Land Bridge interacts with the following contracts: - [FxBaseChildTunnel](https://github.com/fx-portal/contracts/blob/main/contracts/tunnel/FxBaseChildTunnel.sol) - [FxBaseRootTunnel](https://github.com/fx-portal/contracts/blob/main/contracts/tunnel/FxBaseRootTunnel.sol) The scope of the audit treats 3rd party entities as black boxes and assumes their functional correctness. However, in the real world, 3rd parties can be compromised, which may lead to lost or stolen assets.
Open the reportIn the current implementation, when the `LandSwap` contract is not in the pause condition, the `burn()` function allows everyone to burn `oldLand` tokens with given `ids`. The `LandSwap` contract receives `oldLand` tokens during the `swap()` invocation and those tokens are supposed to be burnt immediately. The concern is if those `_oldland` tokens are still valuable to some extent (f.e., causing market fluctuation), a user might call this function at a certain time for his own profit.
Open the reportIn `LandSwap.sol`, no checks are performed on input parameters to ensure that those are ones that are valid. In the `LandSwap` contract, in the `initialize()` function : - `admin` and `trustedForwarder` addresses should be non-zero addresses; - `oldLand` should not be the same address as `newLand`; - `admin` should not be the same address as `trustForwarder`.
Open the reportThe contract serves as the underlying entity to interact with the following third-party protocols. - `_oldLand` - `_newLand` - `trustedForwarder` The scope of the audit treats third-party entities as black boxes and assumes their functional correctness. However, in the real world, third parties can be compromised, which may lead to lost or stolen assets. For instance, if the `trustedForwarder` is initialized to a malicious address, it could cause unexpected errors during the land-swapping process.
Open the reportThe implementation of `_metaTransactionContracts[msg.sender]` is unknown. In the case that `metaTransaction` contracts are public and propose a `call()` feature, users could call a `metaTransaction` contract, having their call being forwarded to `ERC721BaseToken`. In this scenario, `msg.sender` received by `ERC721BaseToken` would be the address of `metaTransaction`, which would verify the condition `require(_metaTransactionContracts[msg.sender]`. If the check can be bypassed through this way, this would be problematic in the functions using this check: - `ERC721BaseToken`.`approveFor()`; - `ERC721BaseToken`.`_checkTransfer()`; - `ERC721BaseToken`.`_batchTransferFrom()`; - `ERC721BaseToken`.`setApprovalForAllFor()`; - `ERC721BaseToken`.`burnFrom()`. For example, ```solidity=301 function setApprovalForAllFor( address sender, address operator, bool approved ) external { require(sender != address(0), "Invalid sender address"); require( msg.sender == sender || _metaTransactionContracts[msg.sender] || _superOperators[msg.sender], "not authorized to approve for all" ); _setApprovalForAll(sender, operator, approved); } ``` The above function will successfully triggered if the caller is from meta transaction contract (i.e., `_metaTransactionContracts[msg.sender]` is `true`).
Open the reportThe `PausableWithAdmin` contract has an unlocked compiler version. An unlocked compiler version in the source code of the contract permits the user to compile it at or above a particular version. This, in turn, leads to differences in the generated bytecode between compilations due to differing compiler version numbers. This can lead to ambiguity when debugging as compiler-specific bugs may occur in the codebase that would be hard to identify over a span of multiple compiler versions rather than a specific one. Additionally, it has been noticed that all contracts are compiled with the compiler version over `0.5.2`, which dates from March 2019. It is recommended to update the compiler versions in the contracts, so they are not exposed to potential security issues related to old compiler versions.
Open the reportSaving information of address owner and bool operatorEnabled in a uint256 is of high efficiency. However, this data structure requires developers to stay aware of the changes when they are trying to make conversion between uint256 and address.
Open the reportThe given input `size` is missing the check for the non-zero address.
Open the reportThe pure functions do not read or modify the state variables, which returns the values only using the parameters passed to the function or local variables present in it.
Open the reportThe view functions are read-only function, which ensures that state variables cannot be modified after calling them.
Open the reportThe change of `admin` by function `changeAdmin()` overrides the previously set `admin` with the new one without guaranteeing the new `admin` can actuate transactions on-chain.
Open the reportIn the constructor, the variable `_admin` is assigned by an explicit `admin` address, and the input is not validated.
Open the reportIn the constructor, the variable `_admin` is assigned by an explicit `admin` address, and the input is not validated.
Open the reportSafeMath from OpenZeppelin is not used in the following functions which makes them possible for overflow/underflow and will lead to an inaccurate calculation result.
Open the reportSafeMath from OpenZeppelin is not used in the following functions which makes them possible for overflow/underflow and will lead to an inaccurate calculation result. - `_burn()` - `_transferFrom()` - `_batchTransferFrom()`
Open the reportSafeMath from OpenZeppelin is not used in the following functions which makes them possible for overflow/underflow and will lead to an inaccurate calculation result. - `x()` - `y()`
Open the reportAccording to the coding practice, the `uint` is an alias for `uint256` and both represent the same underlying integer allocation. It is advisable that for clean coding practices the complete form `uint256` should be used instead of the alias `uint`.
Open the reportThe aforementioned code segments are exactly the same leading to unnecessary bytecode duplication.
Open the reportThe functions “_batchMint” and “batchTransferFrom” are similar, however they utilize a different variable to conduct the logic and represent the maximum of “uint256” in two different ways (“2**256 - 1” and “0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF”).
Open the reportThe statements between L26 and L29 can be grouped to a single instantiation and assignment as in its current state it conducts a full word update operation on each line.
Open the reportWithin the smart contracts, two bit-wise operands that are contradictory exist named “IS_NFT” and “NOT_IS_NFT”.
Open the reportThe function “_checkGemsQuantities” is meant to check the “gemQuantities” provided as arguments to the function, however it also alters them by subtracting 1 on each quantity before returning them.
Open the reportThe function “getValues” internally finds the maximum gem ID possible, instantiates an empty in-memory array with a length equal to that ID plus one and proceeds to utilize the array as a "mapping" of a gem's ID with its computed value.
Open the reportThe statements of L67 conduct the same conditional checks whereby “allowance” is ensured to be greater than or equal to amount internally within the sub function of “SafeMathWithRequire”.
Open the reportThe structure of the codebase does not conform to the official Solidity style guide of v0.6.5. An indicative excerpt of the style guide is that functions should be grouped according to their visibility and ordered: * constructor * receive function (if exists) * fallback function (if exists) * external * public * internal * private Additionally, the internal layout of a contract should be as follows: * Type declarations * State variables * Events * Functions However, these paradigms are not followed, increasing the illegibility of the codebase and rendering it harder to orient oneself within the codebase.
Open the reportWithin the codebase, multiple types of “return” variables are explicitly named in the function signature, however they are never utilized.
Open the reportThe functions “isApprovedForAll”, “isAuthorizedToTransfer” and “isAuthorizedToApprove” are functions that could be used within the require statements of L150 to L157, L178 to L181, L223, L257 and L274. Additionally, “isAuthorizedToTransfer” internally can call “isAuthorizedToApprove”.
Open the reportThe function “addCatalysts” accepts a “valueOverrides” array that signifies whether the values of a particular catalyst should be overridden. However, the logical check of L21 to L23 would make it impossible to override the value of the f.e. third catalyst without overriding all preceding values.
Open the reportThe functions “buyLandWithSand”, “buyLandWithETH”, “buyLandWithDAI” don’t check whether the input referral has been processed hence we need to be sure that an unauthorized user cannot call these functions more than the intended number of times.
Open the reportIn the function “buyLandWithETH” it can happen that “msg.sender” is a contract, hence “msg.sender.transfer(msg.sender - ETHRequired)” would trigger the receive or fallback function and can lead to revert. Nowaday “.transfer” only forwards 2300 gas, hence reentrancy is not a threat here.
Open the reportThe function `_checkAdjacency` always checks the adjacency of each newly added quad towards the last quad in the estate. Because of this the respective junction is left out which makes the junctions and quad lists unequal in length and usage of “_checkAdjacency” unintuitive
Open the reportThe function “approve” cannot be used by “_metaTransactionContracts” even though it is allowed in “approveFor” and we can just set “sender” to be the owner of the estate in “approveFor” to have the same effect for “approve”.
Open the reportThe implementation of “check_burn_authorized” , “check_add_authorized” and “check_create_authorized” indicates that Meta Transaction Contract, Superoperators, Operators for all, Operators, or even the owners of the estates are deprived of respective burning, adding, creating rights whenever a fixed global minter or breaker are set. Is this behaviour intended?
Open the reportThe reports name a repository or files. Open a report to see its scope.
Is this your project? Show this record on your own website. The label states what the record holds, links to this page and updates by itself. It is free and needs no sign-in.
Automatic: the three strongest facts of this project’s own record (its last audit and two more), highlighted. They update with the record. Picking a fact turns Automatic off.
<head><script src="https://widget.trustblock.run/project-widget.min.js" defer></script></head><body><divdata-tb-project-widgetdata-tb-project-slug="the-sandbox"></div></body>
Paste the code where the label should appear. It uses no API credit and updates by itself.
Something wrong on this page? A missing report, a wrong date, a finding that was fixed since. Tell us and we check it against its source.
Tell us