As of 3 Nov 2026, DeXe Protocol has 3 audit reports by 2 security firms on Trustblock; the latest is by Hacken, dated 19 Jun 2023. Those reports list 2 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 |
|---|---|---|---|---|---|
| DeXe / DEX | Security firm: Hacken | Date: | Scope: 1 address on Ethereum | 2 open critical findings | Open · hacken.io |
| DeXe Audit | Security firm: CertiK | Date: | Scope: Files named in the report | No open critical findings | Open · skynet.certik.com |
| DeXe / Token sales | Security firm: Hacken | Date: | Scope: 1 address on Ethereum | No open critical findings | Open · hacken.io |
Statuses are the security firm’s own, from each report.
| Severity | Open | Acknowledged | Fixed |
|---|---|---|---|
| Critical | 2 | 0 | 0 |
| High | 7 | 1 | 8 |
| Medium | 2 | 0 | 6 |
| Low | 18 | 0 | 18 |
The report gives no description here.
Open the reportThe report gives no description here.
Open the reportThe report gives no description here.
Open the reportThe report gives no description here.
Open the reportThe report gives no description here.
Open the reportThe report gives no description here.
Open the reportThe report gives no description here.
Open the reportAccording to the design of `GovPool`, the creator of the gov pool has the option to set the `earlyCompletion` parameter to true and the `validatorsVote` parameter to false, indicating that voting will complete as soon as the quorum is achieved without an additional validator step. However, this option can make the proposal vulnerable to flashloan attacks, where someone can instantly borrow large amounts of tokens to reach the quorum and execute the proposal. ```solidity=387 if (core.settings.earlyCompletion || voteEnd < block.timestamp) { if (_quorumReached(core)) { if (core.settings.validatorsVote && _govValidators.validatorsCount() > 0) { ... } else { return ProposalState.Succeeded; } } ``` Additionally, according to the design of the `GovPoolCreate` library, the function determines the type of proposal based solely on the value of `executors[executors.length - 1]`. If the value matches the address of `GovSetting`, `GovUserKeeper`, or `GovPool` contract, the proposal is considered internal. The `_handleDataForInternalProposal()` function then checks whether the proposal's `data` is legitimate. For instance, only the `setERC20Address()` and `setERC721Address()` functions in the `GovUserKeeper` contract can be executed through proposals. However, an attacker can change the value of `executors[executors.length - 1]` to any other contract and place the address of `GovUserKeeper` contract in another position. As a result, the `_handleDataForInternalProposal()` will be bypassed, allowing the attacker to perform other dangerous operations in the `GovUserKeeper` contract, such as `unlockToken()` and `withdrawToken()`, to steal the deposited tokens from other users. ```solidity=118 function _validateProposal( address[] calldata executors, uint256[] calldata values, bytes[] calldata data ){ ... address mainExecutor = executors[executors.length - 1]; settingsId = IGovSettings(govSettings).executorToSettings(mainExecutor); ... if (settingsId == uint256(IGovSettings.ExecutorType.INTERNAL)) { _handleDataForInternalProposal(govSettings, executors, values, data); } else if (...){ ... } ... } function __handleDataForInternalProposal(){ ... require( values[i] == 0 && executorSettings == uint256(IGovSettings.ExecutorType.INTERNAL) && (selector == IGovSettings.addSettings.selector || selector == IGovSettings.editSettings.selector || selector == IGovSettings.changeExecutors.selector || selector == IGovUserKeeper.setERC20Address.selector || selector == IGovUserKeeper.setERC721Address.selector || selector == IGovPool.editDescriptionURL.selector || selector == IGovPool.setNftMultiplierAddress.selector || selector == IGovPool.changeVerifier.selector), "Gov: invalid internal data" ); ... } ```
Open the reportThe report gives no description here.
Open the reportThe report gives no description here.
Open the reportThe report gives no description here.
Open the reportThe report gives no description here.
Open the reportThe report gives no description here.
Open the reportThe report gives no description here.
Open the reportThe report gives no description here.
Open the reportThe report gives no description here.
Open the reportThe report gives no description here.
Open the reportAccording to the logic of `TraderPoolExchange`, the `exchange()` function facilitates the exchange of `from` token to `to` token in the trader pool. Input validation is performed to ensure that the `from` token and `to` token are not blacklisted. ```solidity=43 require( !coreProperties.isBlacklistedToken(from) && !coreProperties.isBlacklistedToken(to), "TP: blacklisted token" ); ``` ```solidity=79 if (from != poolParameters.baseToken && from.thisBalance() == 0) { positions.remove(from); emit PositionClosed(from); } ``` However, if the `coreProperties` contract adds new blacklisted tokens via the `addBlacklistTokens()` function after an exchange operation, traders will be unable to close blacklisted position tokens since the remove operation in line 80 will never be reached. Consequently, the condition `traderPool.openPositions().length == 0` will never be true, and traders will be unable to perform divest operations in the `divest()` function of the `TraderPoolDivest` library. ```solidity=40 require(!senderTrader || traderPool.openPositions().length == 0, "TP: can't divest"); ```
Open the reportPerforming integer division before multiplication truncates the low bits, losing the precision of calculation. ## Function `buy()`: The linked code implements the calculation: $\text{saleTokenAmount} = \text{amount} * \text{exchangeRate} / \text{PRECISION}$. e.g. Suppose - amount = 15 - exchangeRate = 1/2 * PRECISION - tierView.vestingSettings.vestingPercentage = 2/10 * PERCENTAGE_100 We have `saleTokenAmount = 15 * 1/2 * PRECISION / PRECISION = 7`. Then `saleTokenAmount.percentage(PERCENTAGE_100 - tierView.vestingSettings.vestingPercentage) = 7 * (PERCENTAGE_100 - 2/10 * PERCENTAGE_100)/PERCENTAGE_100 = 5`. If the function applies multiplication before division, then the aforementioned result will be `6` instead of `5`. ## Function `_countPrefixVestingAmount()`: If `vestingSettings.unlockStep` is not a divisor of `vestingSettings.vestingDuration`, `purchase.vestingTotalAmount` may never be reached. e.g. Suppose - unlockStep == 3 - vestingDuration == 10 - vestingTotalAmount == 10 We have `stepsCount == vestingDuration / unlockStep = 10/3 = 3` and `tokensPerStep == vestingTotalAmount / stepsCount = 10/3 = 3` Then the maximum return value of `_countPrefixVestingAmount()` is `(vestingDuration / unlockStep) * tokensPerStep = (10 / 3) * 3 = 3 * 3 = 9 < vestingTotalAmount`.
Open the reportAccording to the entire protocol's design, most internal calculations utilize 18 decimal places for tokens, with the exception of the TokenSaleProposal contract. In particular, the exchange rate between `tokenToBuyWith` and `saleToken`, as well as the transfer operations within this contract, may handle tokens incorrectly.
Open the reportIf the current time `block.timestamp` is before the `vestingView.cliffEndTime`, the `nextUnlockTime` is calculated as ```solidity=257 vestingView.nextUnlockTime = purchase.purchaseTime + uint64(uint256(vestingSettings.cliffPeriod).max(vestingSettings.unlockStep)); ``` However, `unlockStep` represents the tick step with which vested funds are given to the buyer, so it should instead be `purchase.purchaseTime + uint64(uint256(vestingSettings.cliffPeriod).max(vestingSettings.vestingDuration / vestingSettings.unlockStep))`. Similarly for the case when the current time `block.timestamp` is at least `vestingView.cliffEndTime`, `vestingSettings.unlockStep` should be replaced with `vestingSettings.vestingDuration / vestingSettings.unlockStep`. Another issue is that even with the above changes, the calculation of the unlock time can still be incorrect due to integer truncation. Let's suppose all mentions of `unlockStep` are replaced with `vestingSettings.vestingDuration / vestingSettings.unlockStep`. If `unlockStep` is not a divisor of `vestingDuration`, then it is possible for `nextUnlockTime` to be incorrect. The following example goes through the calculations shown in the code snippet: ```solidity=261 vestingView.nextUnlockTime = uint64(block.timestamp) + vestingSettings.unlockStep; vestingView.nextUnlockTime -= (vestingView.nextUnlockTime - purchase.purchaseTime) % vestingSettings.unlockStep; if (vestingView.nextUnlockTime > vestingView.vestingEndTime) { vestingView.nextUnlockTime = 0; } ``` e.g. Suppose - purchaseTime == 0 - vestingDuration == 10 - unlockStep == 3 - no cliff period - block.timestamp == 9 Then we have `vestingEndTime == 10`. In the calculation of `nextUnlockTime`, we have `nextUnlockTime == block.timestamp + vestingDuration / unlockStep = 9 + 10/3 = 12`. `nextUnlockTime -= (nextUnlockTime - purchaseTime) % (vestingDuration / unlockStep) = 12 % 3 = 0`. So `nextUnlockTime` is unchanged (still 12). As `nextUnlockTime == 12 > 10 == vestingEndTime`, we change `nextUnlockTime` to 0, when it should actually be 10.
Open the reportThere is no storage gap preserved in the logic contract. Any logic contract that acts as a base contract that needs to be inherited by other upgradeable child should have a reasonable size of storage gap preserved for the new state variable introduced by the future upgrades.
Open the reportThe `UserRegistry` contract stores the hash of the privacy policy in the variable `documentHash`, which users must agree to by signing the `Agreement` struct (using EIP712) to verify their agreement. Once the signature is verified, its hash is stored in `userInfo[msg.sender].signatureHash`. The view function `agreed()` can determine if a user has agreed to the policy by checking if `userInfor[msg.sender].signatureHash` is non-zero. ```solidity=53 function agreed(address user) external view override returns (bool) { return userInfos[user].signatureHash != 0; } function setPrivacyPolicyDocumentHash(bytes32 hash) external override onlyOwner { documentHash = hash; } ``` However, there is no validation to confirm the relationship between `signatureHash` and the current `documentHash`. The owner of the contract can change the `documentHash` using the function `setPrivacyPolicyDocumentHash()`, meaning that users who agreed to the previous policy will also agree to updates made to the policy.
Open the reportThe linked code allows `DistributionProposal` contract to transfer a certain amount of `rewardToken` tokens from `govAddress` to this contract. However, `approve()` operation is missing to grant approval to `DistributionProposal` contract. Without sufficient allowance, `DistributionProposal` contract is unable to transfer `rewardToken` tokens from `govAddress` account. ```solidity=75 if (address(rewardToken) == ETHEREUM_ADDRESS) { (bool status, ) = payable(voter).call{value: reward}(""); require(status, "DP: failed to send eth"); } else { if (balance < reward) { rewardToken.safeTransferFrom(govAddress, address(this), reward - balance); } rewardToken.safeTransfer(voter, reward); ```
Open the reportAccording to the design of the `GovValidators` contract, the `createExternalProposal` contract receives the proposal from the `GovPool` contract, storing it in `_externalProposals[proposalId]` for voting. Once approved, the proposal can be executed through the `execute()` function in the `GovPool` contract. However, the `execute()` function does not update the state of `executed` in `_externalProposals[proposalId]`, which can lead to inaccuracies in the `getProposalState()` function. ```solidity=122 _externalProposals[proposalId] = ExternalProposal({ core: ProposalCore({ voteEnd: uint64(block.timestamp + duration), executed: false, quorum: quorum, votesFor: 0, snapshotId: uint32(govValidatorsToken.snapshot()) }) }); ```
Open the reportAccording to the design of the `TraderPoolInvestProposal` contract, the `convertInvestedBaseToDividend()` function is to convert newly invested funds to rewards. Reward distribution of the base token can be very different depending on when `convertInvestedBaseToDividends()` is called. For example, suppose userA is the first user to invest base tokens and userB is the second user to invest base tokens. Case 1: `convertInvestedBaseToDividends()` is called after userA but before userB - userA's investment will be given as rewards only to userA - If `convertInvestedBaseToDividends()` is called after userB invests, userB's investment is given to both userA and userB as rewards Case 2: `convertInvestedBaseToDividends()` is called after both users invest and not before - userA's investment and userB's investment will be given as rewards to both userA and userB So the comparison of the two is that userA gets more rewards in Case 1 and less in Case 2 while the opposite is true for userB.
Open the reportAccording to the design of the library `TraderPoolDivest`, the `_checkUserBalance()` function verifies whether the LP token amounts to be divested are less than the user's LP token balance (minus the amount of LP tokens received through investment in the current block) to prevent users from investing and divesting tokens in the same block. ```solidity=133 require( amountLP <= traderPool.balanceOf(msg.sender) - traderPool.investsInBlocks(msg.sender, block.number), "TP: wrong amount" ); ``` However, if the traderPool's LP token can be borrowed through a flashloan, the value of `traderPool.balanceOf(msg.sender)` may be increased to an arbitrary value, which can bypass the `_checkUserBalance()` function.
Open the reportThe function `canParticipate()` checks if the `voter` has sufficient balance to participate in voting. However, the value of ` ERC20(tokenAddress).balanceOf(voter)` in the `tokenBalance()` function can be manipulated by the `voter` through a flashloan if `isMicropool` is set to `false`. The `voter` can take out a flash loan and use the borrowed funds to increase the `tokenBalance()`, then the following check can be easily bypassed: ```solidity=547 (uint256 tokens, ) = tokenBalance(voter, isMicropool, useDelegated); if (tokens >= requiredVotes) { return true; } ``` ```solidity=404 function tokenBalance( address voter, bool isMicropool, bool useDelegated ) public view override returns (uint256 totalBalance, uint256 ownedBalance) { if (tokenAddress == address(0)) { return (0, 0); } totalBalance = _getBalanceInfoStorage(voter, isMicropool).tokenBalance; if (!isMicropool) { if (useDelegated) { ... } ownedBalance = ERC20(tokenAddress).balanceOf(voter).to18( ERC20(tokenAddress).decimals() ); totalBalance += ownedBalance; } } ``` This allows the user to create a proposal when they may not be able to.
Open the reportAccording to the design of the `TraderPoolInvestProposal` contract, the purpose of the divest() function is to transfer profits from the specified investment proposal to the main pool and users. Upon calculating the rewards, the variable `claim[0]` - the amount of baseToken to be rewarded - is subtracted from both the `_proposalInfos[proposalId].lpLocked` and `totalLockedLP`. However, these two variables serve as trackers for changes in the trader pool's LP token. The function `min()` is utilized to prevent underflow errors, given that the reward base token could potentially exceed the invested base token. However, these two public variables may not be accurate. ```solidity=144 _proposalInfos[proposalId].lpLocked -= claimed[0].min( _proposalInfos[proposalId].lpLocked ); _updateFromData(user, proposalId, claimed[0]); investedBase -= claimed[0].min(investedBase); totalLockedLP -= claimed[0].min(totalLockedLP); ```
Open the reportIn the contract `GovUserKeeper`, the function `delegateTokens()` and `delegateNfts()` allow a delegator to delegate a certain amount of tokens or Nfts to a specified `delegatee`. The functions `undelegateTokens()` and `undelegateNfts()` can undelegate tokens or Nfts from a `delegatee`. However, a delegatee is able to call the function `createProposal()` in the contract `GovPool` to create a proposal. By voting on the created proposal, the `delegatee` updates the value of `micropoolInfo.maxTokensLocked`. Consequently, the delegators are unable to undelegate their tokens and NFTs since the following condition cannot be true: ```solidity=149 uint256 availableAmount = micropoolInfo.tokenBalance - micropoolInfo.maxTokensLocked; require( amount <= delegated && amount <= availableAmount, "GovUK: amount exceeds delegation" ); ```
Open the reportThe linked function `lock()` transfers NFT from `msg.sender` to `address(this)`. However, there is no other function in place to transfer the locked NFT back to its original owner. ```solidity=25 function lock(uint256 tokenId) external override { require( !isLocked(_latestLockedTokenIds[msg.sender]), "ERC721Multiplier: Cannot lock more than one nft" ); _transfer(msg.sender, address(this), tokenId); ... ```
Open the reportThe report gives no description here.
Open the report| Address | Chain |
|---|---|
| 0xde4e…cbd60xde4ee8057785a7e8e800db58f9784845a5c2cbd6 | Chain: Ethereum |
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="dexe-protocol"></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