As of 3 Nov 2026, Reach has 2 audit reports by 1 security firm on Trustblock; the latest is by Quantstamp, dated 31 Jan 2024. Those reports list no open critical findings.
Record last updated . It holds audits and findings, contracts and incidents up to Nov 2026. Trustblock does not review code.
Newest first
| Audit | Security firm | Date | Scope | Critical findings | Report |
|---|---|---|---|---|---|
| /Reach - Diff 2 | Security firm: Quantstamp | Date: | Scope: Files named in the report | No open critical findings | Open · certificate.quantstamp.com |
| /Reach - Diff 1 | Security firm: Quantstamp | Date: | Scope: Repository at commit e7306b8 | No open critical findings | Open · certificate.quantstamp.com |
Statuses are the security firm’s own, from each report.
| Severity | Open | Acknowledged | Fixed |
|---|---|---|---|
| Critical | 0 | 0 | 0 |
| High | 1 | 0 | 4 |
| Medium | 1 | 5 | 2 |
| Low | 0 | 3 | 23 |
The off-chain system generating the Merkle trees and distributions is out of the scope of this audit, but as stated by the Reach team, non-claimed rewards will be included in the next distribution. An exploitable behavior has been identified where a user can intentionally delay claiming rewards until observing a new `createDistribution()` transaction by the system owner in the mempool. Subsequently, the user can claim their un-claimed rewards before the execution of `createDistribution()` and then claim them again after the distribution creation process.
Open the report`ReachDistributionV2` is deployed by `ReachFactory`, making the `ReachFactory` contract the owner. However, the `ReachDistributionV2` contract is intended to be owned by the user who is creating the Reach server. Furthermore, the `ReachFactory` is not configured to call the guarded `onlyOwner` functions in `ReachDistributionV2`.
Open the reportThe `ReachFactory` implements the `topUp()` and `reserveEthAllocations()`functions, which will transfer user funds to the contract. However, the expected behavior is not defined in the documentation, and there is no way of withdrawing the funds from this contract, thereby locking them forever.
Open the reportThe `onlySigned` modifier validates the `_signature` and `_data` has been correctly signed by the `msg.sender`. However, the signatures are not marked as used, allowing them to be replayed at a later date. Additionally, the chain ID is not included either, allowing signatures to be used across chains. This vulnerability allows users to call `topUp()` and `reserveEthAllocation()` with the same signature, allowing them to reset their balances to a higher previous balance which might lead to a loss of funds.
Open the reportIn `ReachAffiliateDistribution.swapEth()`, part of the `_ethAmount` is transferred to the main distribution contract. Specifically, `uint256 commissionAmount = (_ethAmount * commission) / 100;` is transferred to the `mainDistribution` contract. However, in the lines that follow, it swaps `_ethAmount` of ETH for `REACH` tokens, without accounting for the `commissionAmount` that was already transferred out, which can lead to serious accounting issues because the contract might not have enough ETH for the swap. ``` router.swapExactETHForTokensSupportingFeeOnTransferTokens{ value: _ethAmount }(_outputAmount, path, address(this), block.timestamp + 5); ``` Furthermore, if the extra ETH amount that is being swapped is part of the user’s distribution, this could block some users from claiming their rewards.
Open the reportThe project does not implement tests for the new functionalities related to the `ReachDistribution` and `ReachFactory` contracts. Additionally, the test for the `Reach` token contract is very limited, only testing its deployment. Tests may express requirements and are necessary to validate the software's intent.
Open the reportDead code should be avoided since it might consume more resources during development, testing, and compilation without contributing to the software's functionality. It makes the codebase harder to understand and may mislead developers, leading to potential bugs or wasted time. The reader might also believe that the code is incomplete. In the case of solidity, it might also increase the bytecode size increasing deployment costs. The variable `transferThreshold`, the import `EnumerableSet`, and the data structure `Config` are not used. Additionally, both distribution contracts have the variable `totalEthAllocated` whose goal and functionality are unclear.
Open the reportIt is unclear why `_ethAmount` is not used in the verification in `createDistribution`. It is likely that `if (address(this).balance < totalEthAllocated)` should be replaced with `if (address(this).balance < _ethAmount)` to ensure that the distributed amount is less than the available balance in the contract. This can potentially lead to an accounting issue. ``` function createDistribution( bytes32 _merkleRoot, uint256 _ethAmount, uint256 _reachAmount ) public onlyOwner { ... if (address(this).balance < totalEthAllocated) revert UnsufficientEthBalance(); ... } ```
Open the reportAs discussed with the Reach team, the goal `totalEthAllocated` logic in `ReachAffiliateDistribution` is to avoid allocating funds in distributions that will be used to buy back Reach tokens using `swapEth()`. However, as shown in several issues above, the variable introduced various issues. We concluded that this logic is not designed properly to achieve that goal, and should be revised.
Open the reportCode duplication is detrimental as it leads to the maintenance of the same issues in different locations, and is therefore, error-prone. It makes code harder to understand and increases development time. In the case of solidity, it might also lead to larger bytecode which might hinder the deployment of the system. The contracts `ReachAffliateDistribution.sol` and `ReachMainDistribution.sol` share a lot of the same code. It is advisable to utilize inheritance to facilitate the reuse of code, which will make the codebase easier to maintain in the future.
Open the reportIn `swapEth()`, there is this line `totalEthAllocated += address(this).balance - _ethAmount;`. Note that if `_ethAmount` is not equal to `address(this).balance`, the value `totalEthAllocated` will be double counting the contract's balance when `swapEth()` is called multiple times. For example, suppose the contract has a balance of `10 ETH` and `totalEthAllocated = 0`. The owner calls `swapEth()` with `_ethAmount = 1`. This updates `totalEthAllocated` to be `10 - 1 = 9`. The owner again calls `swapEth()` with `_ethAmount = 1`, this updates `totalEthAllocated` to be `9 + (9 -1) = 17`, which is double counting the `8 ETH` balance in the contract across the two `swapETH()` calls.
Open the reportIn the documentation, it mentioned that each mission credit costs `100 REACH` tokens (see [here](https://docs.getreach.xyz/lang/overview/mission-types)). However, in `ReachMainDistribution.sol`, we have `uint256 public creditPrice = 50 ether;`, meaning that each credit costs `50 REACH` tokens.
Open the reportBased on the context of how the commission is used in `ReachAffliateDistribution.sol`, `commission` represents a percentage, which is denominated by `100`. However, in `setCommission()`, we can set the commission to be over `100`, which can lead to unexpected behavior, such as potentially reverting on this line ` payable(mainDistribution).transfer(commissionAmount);` in `swapEth()`.
Open the reportIf the owner renounces their ownership, all ownable contracts will be left without an owner. Consequently, any function guarded by the `onlyOwner` modifier will no longer be able to be executed.
Open the reportThe function `deployAffiliateDistribution()` deploys a new `ReachAffiliateDistribution` for the caller, and stores the address in `affiliates[_name]`. However, there is no check that the given name was already used, which would result in the possible overwrite of said mapping. Additionally, since the mapping cannot be edited manually, the initial association cannot be reestablished anymore.
Open the reportThe project’s configuration and dependency management are not maintained properly. Specifically, our team is not able to install all the dependencies necessary to compile the project, and therefore, it is not possible to compile and run the tests as-is in a fresh development environment. A proper development framework streamlines setup, ensures consistency, facilitates easier testing, accelerates onboarding, and enhances productivity, significantly improving software quality and project collaboration efficiency.
Open the report`ReachAffiliateDistribution.swapEth()` calls Uniswap V2 router to swap ETH for REACH tokens. `router.swapExactETHForTokensSupportingFeeOnTransferTokens()` receives a `deadline` parameter that will be verified when the transaction is mined. However, passing a time relative to `block.timestamp` (i.e. `block.timestamp + 5`) does not make sense as `block.timestamp` is set when the transaction is mined, so the deadline will never expire.
Open the reportThe functions `address.transfer()` and `address.send()` assume a fixed amount of gas to execute the call (2300). The use of these functions protects against reentrancy attacks. However, if the recipient of the transfer is a smart contract implementing some logic in the receiving function (`fallback()`, `receive()`), the transfer will fail.
Open the reportThe function `Reach.rescueETH20Tokens()` suggests it rescues `ETH` but it rescues `ERC20` tokens that are accidentally sent to the contract.
Open the reportThe owner of the contracts inheriting from OpenZeppelin's Ownable can call `transferOwnership()` to transfer the ownership to a new address. If an uncontrollable address is accidentally provided as the new owner address then the contract will no longer have an active owner, and functions with the `onlyOwner` modifier can no longer be executed. While the implementation of `ReachDistributionV2.transferOwnership()` is a good practice (time lock), the risk of transferring it to the wrong address is still possible.
Open the report"Dead" code refers to code that is never executed and hence makes no impact on the final result of running a program. Dead code raises a concern, since either the code is unnecessary or the necessary code's results were ignored. Here are some snippets of dead code found in the codebase: 1. `addLiquidity()` is a private function not called in the contract. 2. In `swapAndLiquify()` there is commented-out code. 3. `claimEnabled` is not used which would make its setter function `setClaimEnabled()` also unnecessary. 4. `devWallet` is not used which would make its setter function `setDevWallet()` also unnecessary. 5. `airdropTokens()` can be removed as it is not used and acknowledged by the Reach team. 6. `EnumerableSet` import can be removed from `ReachDistributionV2.sol`. 7. Taxes in `Reach` have two fields: `treasury` and `liquidity`. However, in the protocol, only the total fee (sum of both) is used.
Open the reportIn `ReachDistributionV2`, `setEthDistribution()` and `setReachDistribution()` are public functions protected by the `onlyOwner` modifier. These allow the owner to change the Merkle roots for both `ETH` and `REACH` allocations, breaking the flow of distribution version creation that should be modified only by `createDistribution()`.
Open the reportAlthough `Reach.rescueETH20Tokens()` is intended to just rescue ERC-20 tokens sent to the contract by error, it does not validate the return value of the transaction. Most of the ERC-20 implementations return `true` if the transfer was successful, following the ERC-20 standard. However, some tokens do not follow this recommendation (e.g. USDT), causing DoS and locked tokens in some smart contracts.
Open the reportIt is important to validate inputs, even if they only come from trusted addresses, to avoid human error. The following is a non-exhaustive list of functions that should validate their inputs: 1. `ReachFactory.constructor()`: Validate that the `reachToken` is a non-zero address. Since there is no setter for the token address, an incorrect provided value would require a redeployment of the factory. 2. `ReachFactory.setPrices()`: Validate that `_credits` and `_minEthAllocation` are non-zero. 3. `Reach.setSwapTokensAtAmount()`: Validate that the input `amount` lies within a given range with lower and upper values.
Open the reportIf the owner renounces their ownership, all ownable contracts will be left without an owner. Consequently, any function guarded by the `onlyOwner` modifier will no longer be able to be executed.
Open the reportThe codebase at the confirmed commit hash `9d3b7f1` does not compile. There are missing dependencies in the `package-lock.json` and the `yarn.lock` files.
Open the reportEvery Solidity file specifies in the header a version number of the format `pragma solidity (^)0.8.*` or `>=0.8.*`. The caret (`^`) or the `>=` before the version number implies an unlocked pragma, meaning that the compiler will use the specified version _and above_, hence the term "unlocked". There are also inconsistencies in the version to be used between files. The files `ReachDistributionV2.sol`, `ReachFactory.sol`, and `Reach` use `>=0.8.19`. The file `IDex.sol` uses a locked pragma but with a different solidity version `0.8.20`.
Open the reportThe function `getDeployedDistributions()` returns the state variable`deployedDistributions`. However, said variable is already public and solidity always provides getter functions for all public state variables.
Open the reportThe function `ReachDistribution.createDistribution()` allows to create a distribution of both `ETH` and `REACH` at the same time. The function `claimRewards()` allows a user to claim rewards only once with the require statement `require(lastClaimedVersion[msg.sender] < currentVersion, "Already claimed for this version")`. However, if a user is supposed to get rewards in both currencies and for some reason, one Merkle proof was provided incorrectly, the user will get one type of reward and will not be able to claim a second time.
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="reach"></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