UnchainX security record

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.

Critical findings
No open critical findings
Across the 3 reports, 2 critical findings were reported. Each is marked fixed by the firm that reported it. See the findings
Open the latest report
Your label, live
Get the label for your site

Record last updated . It holds audits, findings and contracts up to Nov 2026, and incidents up to Oct 2026. Trustblock does not review code.

Audits3

Newest first

AuditSecurity firmDateScopeCritical findingsReport
Unchain X Circular EcosystemSecurity firm: CertiKDate: Scope: Files named in the report2 critical findings, fixedOpen · skynet.certik.com
Unchain-XSecurity firm: CertiKDate: Scope: 1 address on BNB ChainNo open critical findingsOpen · skynet.certik.com
Unchain-X: LaunchpadStaking auditSecurity firm: PeckShieldDate: Scope: Not recorded (see the report)No open critical findingsOpen · GitHub

Findings42

Statuses are the security firm’s own, from each report.

SeverityOpenAcknowledgedFixed
Critical002
High215
Medium104
Low1026
CriticalCannot Schedule Any State Changing ActionsUnchain X Circular Ecosystem · CertiK · 9 Sep 2024Fixed

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 report
CriticalIncorrect Logic Causes Users To Be Unable To ClaiUnchain X Circular Ecosystem · CertiK · 9 Sep 2024Fixed

The 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 report
HighTimeLock Controller Will Not CompileUnchain X Circular Ecosystem · CertiK · 9 Sep 2024Fixed

The `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 report
HighImproper Reward Calculations in `HalvingProtocol`Unchain-X · CertiK · 24 Jul 2024Fixed

Functions `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 report
HighIncorrect Accounting If Activated/Deactivated MulUnchain-X · CertiK · 24 Jul 2024Fixed

The 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 report
HighIncorrect Accounting If Deactivated After HalvingUnchain-X · CertiK · 24 Jul 2024Fixed

If `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 report
HighIncorrect Allocation If Allocation Is Set Prior TUnchain-X · CertiK · 24 Jul 2024Fixed

The 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 report
Medium`preVotingPowerAllocation` Is Never Set To TrueUnchain X Circular Ecosystem · CertiK · 9 Sep 2024Fixed

The 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 report
MediumIssue If `lmPool` Is Set After Pool CreationUnchain-X · CertiK · 24 Jul 2024Fixed

When 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 report
MediumMaxAllocation Can Be Set Less Than TotalAllocatioUnchain-X · CertiK · 24 Jul 2024Fixed

The 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 report
Show 10 more
MediumPossible Reentrancy During Inconsistent StatesUnchain-X · CertiK · 24 Jul 2024Fixed

The 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 report
Low`DataParams` Does Not Include Parameter For `msg.Unchain X Circular Ecosystem · CertiK · 9 Sep 2024Fixed

The `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 report
LowIf `target` Is An EOA Call Will SucceedUnchain X Circular Ecosystem · CertiK · 9 Sep 2024Fixed

The 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 report
LowMinimum For Voting Token Mint May Cause Users ToUnchain X Circular Ecosystem · CertiK · 9 Sep 2024Fixed

The 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 report
LowMissing Error MessageUnchain X Circular Ecosystem · CertiK · 9 Sep 2024Fixed

The **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 report
LowMissing Input ValidationUnchain X Circular Ecosystem · CertiK · 9 Sep 2024Fixed

In 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 report
LowPercentage Of Shares To Lose When Calling `claim(Unchain X Circular Ecosystem · CertiK · 9 Sep 2024Fixed

The 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 report
LowProtocol May Allocate Rewards For Blocks Prior ToUnchain X Circular Ecosystem · CertiK · 9 Sep 2024Fixed

The 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 report
LowReduction Of Shares Is Dependent On Unclaimed RewUnchain X Circular Ecosystem · CertiK · 9 Sep 2024Fixed

The 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 report
LowSpecific Imports Are Not Used ConsistentlyUnchain X Circular Ecosystem · CertiK · 9 Sep 2024Fixed

Specific 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 report
Show 10 more
Low`delete` Keyword Can Be UsedUnchain-X · CertiK · 24 Jul 2024Fixed

The 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 report
Low`for` Loop OptimizationUnchain-X · CertiK · 24 Jul 2024Fixed

The increment operation saves a small amount of gas if performed from the left (`++i`) instead of from the righthand side (`i++`).

Open the report
LowAccumulating Logic Will Not Give Rewards For TheUnchain-X · CertiK · 24 Jul 2024Fixed

The logic of `accumulateReward()` will not allocate rewards for the genesis block as it assumes it is accumulating starting from the genesis block.

Open the report
LowAll Allocated Rewards May Not Be AccumulatedUnchain-X · CertiK · 24 Jul 2024Fixed

If `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 report
LowDeploy Fee May Be Accidentally BurnedUnchain-X · CertiK · 24 Jul 2024Fixed

If 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 report
LowInconsistent Listing CheckUnchain-X · CertiK · 24 Jul 2024Fixed

The 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 report
LowInconsistent Use Of `onlyOwner`Unchain-X · CertiK · 24 Jul 2024Fixed

In 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 report
LowLast Interval May Be Longer Than Other IntervalsUnchain-X · CertiK · 24 Jul 2024Fixed

In 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 report
LowLow-level Call To Address With No Code Will SuceeUnchain-X · CertiK · 24 Jul 2024Fixed

If 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 report
LowMisleading NameUnchain-X · CertiK · 24 Jul 2024Fixed

The modifier `onlyNFPManagerOrLmFactory` does not describe that the `msg.sender` could also be the `v3Pool`.

Open the report
Show 7 more
LowMissing Emit EventsUnchain-X · CertiK · 24 Jul 2024Fixed

Functions that update state variables should emit relevant events as notifications.

Open the report
LowMissing NatSpec CommentsUnchain-X · CertiK · 24 Jul 2024Fixed

Many contracts in the codebase are missing NatSpec comments.

Open the report
LowNo Cap On FeesUnchain-X · CertiK · 24 Jul 2024Fixed

There 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 report
LowUnnecessary conditionUnchain-X · CertiK · 24 Jul 2024Fixed

In 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
LowUnnecessary InitializationUnchain-X · CertiK · 24 Jul 2024Fixed

`uint256` is by default initialized to `0`, so that it is not necessary to explicitly initialize `totalMiningBeforeLastHalving` to `0`.

Open the report
LowVariables That Could Be Declared as ImmutableUnchain-X · CertiK · 24 Jul 2024Fixed

The 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 report
LowConfiguration Dependency Enforcement in LaunchpadStakingUnchain-X: LaunchpadStaking audit · PeckShield · 23 Apr 2024Fixed

The report gives no description here.

Open the report

Contracts1

AddressChainSource code
0x4f0572ca0bf96f5ae17b7062d97cea3f35bdea6fChain: BNB ChainSource code: Verified source

All 1 address and the files the reports name

Show this on your site

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.

Content

Facts

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.

Fixed as
Language

Style

Layout
Theme
Accent
Corners

Format

Size
Date
Embed as
HTML
<head>
<script src="https://widget.trustblock.run/project-widget.min.js" defer></script>
</head>
<body>
<div
data-tb-project-widget
data-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