As of 3 Nov 2026, UnchainX has 3 audit reports by 2 security firms on Trustblock; the latest is by CertiK, dated 9 Sep 2024. Those reports list no open critical findings. It also holds 1 deployed contract on 1 chain.
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 |
|---|---|---|---|---|---|
| Unchain X Circular Ecosystem | Security firm: CertiK | Date: | Scope: Files named in the report | 2 critical findings, fixed | Open · skynet.certik.com |
| Unchain-X | Security firm: CertiK | Date: | Scope: 1 address on BNB Chain | No open critical findings | Open · skynet.certik.com |
| Unchain-X: LaunchpadStaking audit | Security firm: PeckShield | Date: | Scope: Not recorded (see the report) | No open critical findings | Open · GitHub |
Statuses are the security firm’s own, from each report.
| Severity | Open | Acknowledged | Fixed |
|---|---|---|---|
| Critical | 0 | 0 | 2 |
| High | 2 | 1 | 5 |
| Medium | 1 | 0 | 4 |
| Low | 1 | 0 | 26 |
The function `enqueue()` is designed to schedule an operation with a delay. However, it calls `_validationWithStaticCall()`, which makes the following sequence of static calls ```solidity for (uint256 i = 0; i < data.length; ++i) { (bool success, ) = data[i].target.staticcall(data[i].payload); if (!success) { revert InvalidData(data[i]); } } ``` Similarly `_execute()`, makes the same sequence of calls ```solidity for (uint256 i = 0; i < data.length; ++i) { (bool success, bytes memory returndata) = data[i].target.call(data[i].payload); Address.verifyCallResult(success, returndata); } ``` Both functions aim to execute the provided payloads; however, the static call in `_validationWithStaticCall()` will fail if any state changes occur. This prevents such transactions from being scheduled. Since most payloads should indeed change the blockchain state, this issue is given a *Critical* severity.
Open the reportThe logic of `totalRemainReward()` does not calculate the correct remaining reward due to the following errors in the logic. - The following logic divides the total rewards accumulated for a halving period and divides it by the total share, when it should give the total amount of rewards not the rewards per share. In addition, it should accumulate rewards for the entire halving period, which does not include the block of the halving as this is at the new reward rate. So that `tmpBlock` should be set to `halvingBlocks[i] - 1` and this logic should be executed if `lastBlock >= halvingBlocks[i]`. Not that the incremented amount should be careful to only accumulate rewards for blocks that should have a reward per block given by `rewardPerBlockOf(i)`. ```solidity if(lastBlock > halvingBlocks[i]) { // Calculate reward before halving remains += rewardPerBlockOf(i) * ((halvingBlocks[i] - 1) - tmpBlock + 1) * 1e18 / totalShare; tmpBlock = halvingBlocks[i]; } ``` - The following logic sets the return amount remains to the amount of rewards for since the last halving. However, it should increment by this amount so that it accounts for all rewards. In addition, the logic calls `rewardPerBlock()`, which calls `halvingProtocol.currentRewardPerBlock()` which returns 0 if the current block is greater than the end block. Thus if it is called after the `endBlock` it will add 0 to `remains` and not account for all of the rewards accumulated after the last halving. ```solidity if(tmpBlock <= lastBlock) { remains = rewardPerBlock() * (lastBlock - tmpBlock + 1); } ``` - The following line does not assign the result of the operation to `remains`. As such the amount returned will be the total amount of rewards as opposed to the total amount of remaining rewards. ```solidity remains - claimedReward; ```
Open the reportThe `TimeLockController` contract will not compile and will trigger the compiler error: ```text "Unimplemented feature (/solidity/libsolidity/codegen/ArrayUtils.cpp:227): Copying of type struct IBaseTimeLock.DataParams calldata[] calldata to storage not yet supported." ``` The issue stems from the following line: ```sol=54 _data[id] = params; ``` This line attempts to write a dynamic calldata array to a dynamic storage array, which is currently not supported.
Open the reportFunctions `rewardPerBlockOf()` and `currentRewardPerBlock()` use the following to calculate a reward per block amount: ```solidity reward = initReward / (2 ** halvingNum) / 28800 ``` where `halvingNum` may be replaced by `totalNum` within function `rewardPerBlockOf()` in the case where `halvingNum > totalNum`. This calculation is incorrect for how the `endBlock` is set up in the `constructor`: ```solidity endBlock = options.genesisBlock + (options.halvingInterval * options.totalNum) + ((options.totalSupply - totalMiningBeforeLastHalving) / (options.initReward / (2 ** options.totalNum))) - 1; ``` `endBlock` is composed of the taking the `genesisBlock` and adding the `totalNum` multiple of the `halvingInterval` in order to get to the last halving block; This value is summed with the difference in the total supply and the total mined before the last halving block, then divided by what is assumed to be the reward per block in order to get the number of blocks to be added to the last halving block in order to obtain the `endBlock`. In other words, the rewards per block are assumed to be `initReward / (2 ** halvingNum)` where there is no division by `28800`.
Open the reportThe functions `activate()` and `deactivate()` can be called multiple times via the functions `list()` and `delist()` in the contract `UNXWapV3LmFactory`. However, the logic assumes that if a pool is deactivated, that it will not be activated again. This is because when `deactivate()` is called, it sets `lastActivedBlock` to be the current block number and `actived` to false. Then the function `accumulateReward()` will treat the `lastActivedBlock` as the `endBlock` as opposed to the end block of the halving protocol. The logic will then only accumulate rewards up until the `lastActivedBlock`, and after it is accumulated to this point the function `accumulateReward()` will simply return without updating the `lastUpdateBlock`. Then if `activate()` is called after a pool was deactivated, it will update `actived` to be true, so that it will have the halving end block as the `endBlock`. This will then allow rewards to be accumulated from the `lastUpdateBlock` to the halving end block, which can include time that the pool was deactivated and should not accumulate rewards. As this can allocate funds that are not accounted for, it may cause the `HalvingProtocol` to run out of funds, resulting in users being unable to harvest rewards they are entitled to. In addition, it can cause users to be able to claim additional rewards that they should not be entitled to.
Open the reportIf `deactivate()` is called after the `halvingProtocol.endBlock()`, then it will update `lastActivedBlock` to a block number greater than the `halvingProtocol.endBlock()`. When `accumulateReward()` is called, it will then take the `endBlock`, which is the block to which to accumulate rewards, as the `lastActivatedBlock`. This will accumulate rewards for a larger period than expected and may cause the `HalvingProtocol` to run out of funds, resulting in users being unable to harvest rewards they are entitled to. In addition, it can cause users to be able to claim additional rewards that they should not be entitled to.
Open the reportThe function `allocate()` in the UNXwapV3Manager allows the owner or executor to set an allocation amount for lmPools. This does so by calling `allocate()` in the `UNXwapV3LmFactory`, which performs checks to ensure the total allocation does not exceed the max allocation and calls `setAllocation()` on the individual lmPools. The function `setAllocation` in the `UNXwapV3LmPool` first calls `accumulateReward()` and then sets the new allocation amount. However, if `accumulateReward()` is called when the pool is deactivated, then it will not update the `lastUpdateBlock`. If it is called after the genesis block when the protocol is deactivated it can allow for unintended rewards to be allocated if the protocol is then activated.
Open the reportThe function `preAllocateVotingPower()` checks if `preVotingPowerAllocation` is true to determine if the pre-allocation of voting power has been given. However, `preVotingPowerAllocation` is not set to true within the `preAllocateVotingPower()` function and there is no method in the contract to update it to true. In addition, `preAllocateVotingPower()` calls ` _updateVotes(params[i].account, 0, params[i].votingPower)` which assumes that users have no current votes. As such it should be ensured that this is called prior to `initialize()` being called otherwise if users have called `addShare()` before, then they will lose their votes obtained from burning tokens.
Open the reportWhen a `v3Pool` is created, an associated `lmPool` is created in the function `createPool()`. This `lmPool` is then synced with the state of `v3Pool` and has its liquidity updated via the `NonfungiblePositionManager` when there are updates to the `v3Pool` liquidity. If the function `setLmPool()` is used to change the `lmPool` to another address, then the `lmPool` will no longer be synced with the `v3Pool` or the `NonfungiblePositionManager`. This can make it so that users do not accumulate rewards when they should or accumulate more rewards than they should. In addition, in the `UNXwapV3LmFactory` when an `lmPool` is created it is set in the mapping `lmPools` which is used in the check for `transferReward()` and when listing, delisting, or allocating rewards for a `v3Pool`. The mapping `lmPools` is only set when creating a pool, so that if `setLmPool()` is called these will no longer match.
Open the reportThe function `setMaxAllocation()` does not check if the input `maxValue` is less than the current `totalAllocation`. Thus it is possible that `maxAllocation` is less than the current `totalAllocation`. The `HalvingProtocol` has the ability to have multiple operators to allow the rewarding of the stored `token`. If these operators determine the amount they can allocate by utilizing the `maxAllocation` variable, then this can cause an overallocation of funds and prevent some users from collecting rewards they are entitled to.
Open the reportThe function `_updateLmPosition()` is called last during liquidity operations to call the lmPool to sync its liquidity with that of the v3Pool. However, it is possible to reenter the protocol to execute operations before the v3Pool and its associated lmPool have their liquidity values synced. In particular, if tokens with hooks are supported, during the transfer, they may make an external call allowing users to reenter the protocol. As this happens before the call to `_updateLmPostion()` it allows users to interact with the protocol when the v3Pool and lmPool are not fully synced. While no explicit attack vector was found, due to the time constraints of the audit we were not able to consider all possibilities. Considering the reentrancy allows interaction with the protocol when the lmPool is not fully updated, it could cause rewards to be allocated unexpectedly so we give this a medium severity.
Open the reportThe `DataParams` struct is defined as ```solidity struct DataParams { address target; bytes payload; } ``` So that each scheduled transaction can only specify the `target` of the call and the `payload` to be sent. If a transaction requires native tokens to be sent via `msg.value`, there is no parameter allowing this value to be specifed. So any transactions requiring native tokens will not be able to be supported. If a transaction requires a previous transaction before it can be executed, then there is no parameter to specify the previous transaction so that it can be checked to be completed. Without these checks it is possible that transactions that should be executed in a specific order may be executed out of order and cause unexpected issues.
Open the reportThe client states that there would be no transactions requiring `msg.value`. As such, we assume that no transaction should have a target be an EOA. However, if the `target` is set to an EOA the call will succeed as a low level call to an EOA will return as successful.
Open the reportThe function `mintVotingToken()` includes a variable `minRequireForMint`, which causes a transaction to revert if a user attempts to mint fewer than this specified minimum amount of `votingToken`. This restriction can result in users having a nonzero, insufficient amount of votes that they are unable to utilize for minting additional voting tokens if `minRequireForMint > 1`. For example, if `minRequireForMint = 2` and a user has `votesOf[caller] = 11`, the user might mistakenly call `mintVotingToken(10)`, successfully minting 10 voting tokens and updating their allocation to `votesOf[caller] = 1`. Consequently, the user will be unable to mint the remaining single vote due to the minimum requirement. Similarly, if a user’s balance is exactly `votesOf[caller] = 1`, they will never be able to mint their allocated vote because it does not meet the `minRequireForMint` threshold.
Open the reportThe **require** can be used to check for conditions and throw an exception if the condition is not met, however, the sited line will revert without giving any error message.
Open the reportIn the `TimeLockController` constructor, the values of `minDelay_` and `maxDelay_` are assigned without validation. We recommend adding checks to ensure that these are within acceptable limits and that `minDelay_ < maxDelay_`. In the `_setAllocation()` function of the contract `XRecycling`, `allocation` will be assigned without any validation. We recommend adding checks to ensure that this value is within an acceptable limit. In particular, the value should never be larger than `10000`, which represents 100%.
Open the reportThe function `claim()` is designed to have a user lose the percentage of shares they have determined by the percentage of the total remaining rewards that they are claiming. The following calculation causes the amount of shares for a users to lose when claiming to be rounded up ```solidity uint256 quotient = totalRemainReward() / requiredReward; uint256 subShare = currentShare / quotient; ``` This can lead to an large loss of shares if a user inputs a large percentage of the `totalRemainReward()`. For example, if `totalRemainReward() = 100e18` and `requiredReward = 51e18`, then `quotient = 1.96078431373` which will round down to 1. So that `subShare = currentShare` and a user would need to lose all their shares to claim this amount, when they are only claiming a little over half of the total rewards. Considering users can protect against this with the `minRequiredRemains` input we give this a *Minor* severity.
Open the reportThe function `rewardPerShare()` does not take into account if the `lastUpdatedBlock` is less than the halving protocol `genesisBlock`. If `initialize()` is called and a user calls `addShare()` before the `genesisBlock`, then the contract can distribute more funds than its allocation. Even if `initialize()` is not called prior to the `genesisBlock`, the the value stored it `rewardPerShareStored` will represent the value of rewards were accumulated from block 0.
Open the reportThe amount of shares that a user loses when calling `claim()` is determined by the proportion of the amount of reward they are claiming vs. the total amount of rewards remaining. As such if all users wish to `claim()` at the same time, users who claim prior to others will lose less shares. This incentivizes users to ensure that their call to `claim()` is processed before other users and can cause users whose transactions are processed later to lose an unexpected amount of shares. This is mitigated by including a `minRequiredRemains` input, which a user can use to specify the minimum amount of shares they must have after calling claim. However, we include this in our report to ensure that users are aware of these risks and recognize the importance of carefully selecting the `minRequiredRemains` input to ensure that they do not lose more shares than expected.
Open the reportSpecific imports are recommended to be used to avoid unpredictable pollution of the namespace. For example: `import {Address} from "@openzeppelin/contracts/utils/Address.sol";`
Open the reportThe lines cited above set variables to their default value. The keyword `delete` can be used in their place as it more closely matches the semantics of the logic.
Open the reportThe increment operation saves a small amount of gas if performed from the left (`++i`) instead of from the righthand side (`i++`).
Open the reportThe logic of `accumulateReward()` will not allocate rewards for the genesis block as it assumes it is accumulating starting from the genesis block.
Open the reportIf `lmLiquidity` is 0, then zero rewards are accumulated even if there is a nonzero amount allocated for those blocks. This is possible for example if some users interact directly with the pool to add liquidity, so that their liquidity is not counted in the `lmPool`. This means that there may be some tokens left in the `HalvingProtocol` contract after all rewards are collected after the `endBlock`.
Open the reportIf the `deployable` is set to true, `deployFee` is set to a nonzero value, `deployFeeToken` is set to a token address, and `deployFeeCollector` is not set, then the deployment fee will be sent to the zero address.
Open the reportThe function `list()` checks if the input `v3Pool` is not listed directly, while the function `delist()` checks if the input `v3Pool` is listed by calling the internal function `_isListed()`.
Open the reportIn the contract `NonfungiblePositionManager`, the function `setOwner` uses the modifier `onlyOwner`. However, the function `setTokenDescriptor` explicitly checks the `msg.sender` is the owner as opposed to using the modifier.
Open the reportIn general, the Halving Protocol, will reduce the reward rate by half each duration of the halving interval, until the `options.totalNum` time, in which case the reward rate will continue until the `options.totalSupply` would be distributed. This means that the reward rate for each interval before a halving will be the same, except for the last interval which may be shorter or longer than the other intervals.
Open the reportIf the `deployFeeToken` address does not have code, then the low level call to it will succeed so that the contract will assume that it received payment, when it did not. For example, if the `deployFee` is set to a nonzero value, but `deployFeeToken` has not been set and is the zero address.
Open the reportThe modifier `onlyNFPManagerOrLmFactory` does not describe that the `msg.sender` could also be the `v3Pool`.
Open the reportFunctions that update state variables should emit relevant events as notifications.
Open the reportMany contracts in the codebase are missing NatSpec comments.
Open the reportThere is no limit to the `deployFee` variables in the contract. During the use of function `createPool()` in the case where the contract is `deployable`, anyone can call the function through the `NonfungiblePositionManager` contract, and, if the `msg.sender` has given infinite allowance to the contract, the privileged role can raise the fee high enough to take all of that the funds of the `deployFeeToken` that the `msg.sender` possesses.
Open the reportIn the function, `accumulateReward()`, if `lastestBlock >= targetBlock` it returns and would not reach further logic. Thus to reach the calculation of the `duration = targetBlock - lastestBlock`, it must be that `lastestBlock < targetBlock`. Therefore the duration is always greater than 0 if this code is reached.
Open the report`uint256` is by default initialized to `0`, so that it is not necessary to explicitly initialize `totalMiningBeforeLastHalving` to `0`.
Open the reportThe linked variables assigned in the constructor can be declared as `immutable`. Immutable state variables can be assigned during contract creation but will remain constant throughout the lifetime of a deployed contract. A big advantage of immutable variables is that reading them is significantly cheaper than reading from regular state variables since they will not be stored in storage.
Open the reportThe report gives no description here.
Open the report| Address | Chain | Source code |
|---|---|---|
| 0x4f05…ea6f0x4f0572ca0bf96f5ae17b7062d97cea3f35bdea6f | Chain: BNB Chain | Source code: Verified source |
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="unchain-x"></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