Incident,
29 ETH.
Source: De.Fi incident database.
- Audits before it
- 6.
- Audits since
- 3, the latest dated 21 Sep 2025.
As of 21 Oct 2026, Onyx Protocol has 9 audit reports by 2 security firms on Trustblock; the latest is by PeckShield, dated 21 Sep 2025. Those reports list no open critical findings. It also holds 1 deployed contract on 1 chain and 1 incident (latest 31 Oct 2023).
Record last updated . It holds audits and findings, contracts and incidents up to Oct 2026. Trustblock does not review code.
Newest first
| Audit | Security firm | Date | Scope | Critical findings | Report |
|---|---|---|---|---|---|
| Onyx: Onyx DAO Registry audit | Security firm: PeckShield | Date: | Scope: Not recorded (see the report) | No open critical findings | Open · GitHub |
| Onyx audit | Security firm: PeckShield | Date: | Scope: Not recorded (see the report) | No open critical findings | Open · GitHub |
| XCN Token (Base) - Audit | Security firm: CertiK | Date: | Scope: Not recorded (see the report) | No open critical findings | Open · skynet.certik.com |
| Onyx Protocol - Audit | Security firm: CertiK | Date: | Scope: Files named in the report | No open critical findings | Open · skynet.certik.com |
| XCN Swap - Audit | Security firm: CertiK | Date: | Scope: Files named in the report | No open critical findings | Open · skynet.certik.com |
| Chain Staking | Security firm: CertiK | Date: | Scope: Files named in the report | No open critical findings | Open · skynet.certik.com |
| Chain - XCN Token | Security firm: CertiK | Date: | Scope: Files named in the report | No open critical findings | Open · skynet.certik.com |
| Chain Team Treasury Timelock | Security firm: CertiK | Date: | Scope: Files named in the report | No open critical findings | Open · skynet.certik.com |
| Chain Token | Security firm: CertiK | Date: | Scope: 1 address on Ethereum | No open critical findings | Open · skynet.certik.com |
Statuses are the security firm’s own, from each report.
| Severity | Open | Acknowledged | Fixed |
|---|---|---|---|
| Critical | 0 | 0 | 0 |
| High | 7 | 2 | 4 |
| Medium | 4 | 0 | 4 |
| Low | 21 | 3 | 17 |
The `_addReserves()` function accrues interest and adds reserves by transferring assets from the sender. The formula for the calculation of `exchangeRate` is as follows: $$ exchangeRate = {totalCash + totalBorrows - totalReserves \over totalSupply } $$ ```solidity=342 function exchangeRateStoredInternal() internal view returns (MathError, uint) { uint _totalSupply = totalSupply; if (_totalSupply == 0) { /* * If there are no tokens minted: * exchangeRate = initialExchangeRate */ return (MathError.NO_ERROR, initialExchangeRateMantissa); } else { /* * Otherwise: * exchangeRate = (totalCash + totalBorrows - totalReserves) / totalSupply */ uint totalCash = getCashPrior(); uint cashPlusBorrowsMinusReserves; Exp memory exchangeRate; MathError mathErr; (mathErr, cashPlusBorrowsMinusReserves) = addThenSubUInt(totalCash, totalBorrows, totalReserves); if (mathErr != MathError.NO_ERROR) { return (mathErr, 0); } (mathErr, exchangeRate) = getExp(cashPlusBorrowsMinusReserves, _totalSupply); if (mathErr != MathError.NO_ERROR) { return (mathErr, 0); } return (MathError.NO_ERROR, exchangeRate.mantissa); } } ``` The `getCashPrior()` function returns `totalSupply`, the NFT can not be borrowed, so the `totalBorrows` is always 0. Based on the logic of functions `mintFresh()` and `redeemFreshOToken()`, it can be inferred that the exchange rate between the underlying asset and the token is 1:1. If the `_addReserves()` function is not disabled, the exchange rate will decrease, the return value `redeemAmount` at line 633 in the `OTokenEx` contract may be 0 when the sender redeems underlying asset using the function `redeem()`/`redeems()`. Worse, if `totalReserves` equals `totalCash`, the exchange rate will be 0 and the sender will not be able to mint/redeem.
Open the reportIn the contract `Swap.sol`, the role `Owner` has authority over the following functions: - withdraw() - setPrice() Any compromise to the `Owner` account may allow a hacker to take advantage of this authority and withdraw all the MDT tokens and modify the critical configuration in the contract.
Open the reportFunction `emergencyWithdraw()` does not trigger the `_moveDelegates()` function like the `withdraw()` function does.
Open the reportThe function `getPriorVotes()` contains a `require()` statement that the input `blockNumber` is smaller than the current block number. ```solidity=310 function getPriorVotes(uint256 _pid, address account, uint blockNumber) public view returns (uint256) { require(blockNumber < block.number, "Comp::getPriorVotes: not yet determined"); ... ``` However, in the `propose()` function in the `CHNGovernance` contract, the following lines of code call the `getPriorVotes()` function with `blockNumber` set as `block.number`, which would cause the aforementioned `require()` statement to fail. ```solidity function getVotingPower(address user, uint blockNumber) public view returns (uint256) { return chnTokenStaking.getPriorVotes(poolId, user, blockNumber); } function propose(address[] memory targets, uint[] memory values, string[] memory signatures, bytes[] memory calldatas, string memory description) public returns (uint) { require(getVotingPower(msg.sender, block.number) >= proposalThreshold, "GovernorAlpha::propose: proposer votes below proposal threshold"); ... ```
Open the reportThe input parameter `tokenIndex` is not checked for a valid array index. If the `tokenIndex` is not correct, the `doTransferOut()` function will revert and the user can not redeem the underlying asset.
Open the reportThe functions `setUnderlyingPrice()` and `setDirectPrice()` can be called by anyone as it has no access restriction. This enables anyone to call this and update the `prices` configuration.
Open the reportThe following codes in the function `redeemFresh()/borrowFresh()` do not meet the Checks-Effects-Interactions pattern. ```solidity=787 doTransferOut(borrower, borrowAmount); /* We write the previously calculated values into storage */ accountBorrows[borrower].principal = vars.accountBorrowsNew; accountBorrows[borrower].interestIndex = borrowIndex; totalBorrows = vars.totalBorrowsNew; ``` It only has a reentrancy lock as there is no lock at the Controller level, only the OToken level. If the `OToken` is an ERC777/ERC677 protocol, the reentrancy can happen in function levels of an ERC777/ERC677-based contract, i.e. multiple function calls that are triggered by the hook mechanism of ERC777/ERC677. This issue is possible to happen with all compound forks, but Compound is not affected as they do not list tokens with callback functionality.
Open the reportIn the `add()` and `set()` functions, the flag `_withUpdate` determines if all the pools will be updated. This reliance might lead to significant loss of the reward. For illustration, assume we have only one pool with `pool.allocPoint == 50` and `totalAllocPoint == 50` at the beginning. Now we want to add another pool with `pool.allocPoint == 50`. There will be two scenarios on calculating the pool reward, Case 1: `_withUpdate` is set to `true`. - distribute the reward and update the pool. - add the given pool information Case 2: `_withUpdate` is set to `false`. - add the given pool information (Note: While we focused on the `add()` function, both the `add()` and `set()` functions update `totalAllocPoint`, which is used in calculation of pool rewards in the function `updatePool()` * In Case 1, reward for the first pool is updated in the call to `updatePool()` where `reward = multiplier.mul(rewardPerBlock).mul(pool.allocPoint).div(totalAllocPoint)`. * In Case 2, an update `totalAllocPoint = totalAllocPoint.add(_allocPoint)` is done first. Then `updatePool()` calculates the reward for the first pool: `reward = multiplier.mul(rewardPerBlock).mul(pool.allocPoint).div(totalAllocPoint);` Because the second pool is sharing rewards with the first one, the amount of reward for the first pool becomes half as much as that in the first case.
Open the reportThe `_reduceReservesFresh()` function reduces reserves by transferring to admin. However, the second argument of `doTransferOut()` function is the index of the token, not the reduction amount. Additionally, the `doTransferOut()` function can only transfer one NFT at a time. As a result, the reduction amount should always be 1, and the admin can only transfer their own tokens.
Open the reportThe `claimXcn()` function of the `Comptroller` contract requires that the token must be listed. However, the `pendingXcn()` function does not exclude the unlisted token. The same issue exists with the function `getDailyXCN()`.
Open the reportIn the `distributeSupplierXcn()` function of the `Comptroller` contract, line 1276 covers the case where users supplied tokens before the market's supply state index was set. Rewards the user with `xcnInitialIndex` accrued from the start of when supplier rewards were first set for the market. So the `distributeSupplierXcn()` function of the `OnyxLens` contract should use the same comparison conditions.
Open the reportAddresses should be checked before assignment or external call to make sure they are not zero addresses. ```solidity=51 admin = admin_; ``` - `admin_` is not zero-checked before being used. --- ```solidity=68 implementation = implementation_; ``` - `implementation_` is not zero-checked before being used. --- ```solidity=52 admin = admin_; ``` - `admin_` is not zero-checked before being used. --- ```solidity=69 implementation = implementation_; ``` - `implementation_` is not zero-checked before being used. --- ```solidity=36 target = target_; ``` - `target_` is not zero-checked before being used. --- ```solidity=415 MINT_BASE_TOKEN = mintBaseToken; ``` - `mintBaseToken` is not zero-checked before being used.
Open the reportIn the `oErc721` contract, the sender cannot borrow the assets, so the aforementioned lines are redundant since the `totalBorrows` is always 0.
Open the reportIn the `Comptroller` contract, the function `getXcnAddress()` returns the address `0xc03Fa5e041BE9C8deD030D6dec7936cE67D78ae8` as the XCN token address. However, in the `ComptrollerG3`, `ComptrollerG4`, and `ComptrollerG5` contracts, this function returns the address `0xA2cd3D43c775978A96BdBf12d733D5A1ED94fb18`.
Open the reportThe function `_setMigration()` is not implemented in the implementation contract `OErc721`.
Open the reportThe comment in line 18 and the formula in line 38 show exchange ratio for XCN and MDT is 1 to 1. ```sol= uint256 public mdtPrice = 1000000000000000000; // 1 XCN = 1 MDT ... uint256 _xcnAmount = _amount.mul(mdtPrice).div(10**PRICE_DECIMALS); ``` However, the comment in line 38 shows the ratio is `MDT = 0.5 XCN`. ```sol uint256 _xcnAmount = _amount.mul(mdtPrice).div(10**PRICE_DECIMALS); // Same decimals, and MDT = 0.5 XCN ``` The exchange ratio is inconsistent across the codebase.
Open the reportLiterals with many digits are difficult to read and review.
Open the reportThe `SafeMath` library is used unnecessarily. With Solidity compiler versions 0.8.0 or newer, arithmetic operations will automatically revert in case of integer overflow or underflow.
Open the reportThe comments above the `add()` function express the documented constraint that the same stake token should not be added more than once with the `add()` function. The current implementation is relying on the `owner` to avoid repeatedly adding the same stake token to the pool, as the function will only be called by the owner."
Open the report```solidity=80 require(_rewardPerBlock < _rewardToken.totalSupply(), "Reward per block less than token total supply"); ``` The error message above is incorrect.
Open the reportThere are no checks to validate if a `_pid` exists in the following functions: - `set()` - `pendingReward()` - `updatePool()` - `stake()` - `withdraw()` - `emergencyWithdraw()` - `claimRewardFromVault()`
Open the report```solidity=112 function getMultiplier(uint256 _from, uint256 _to) public view returns (uint256) { if (_to <= bonusEndBlock) { return _to.sub(_from).mul(BONUS_MULTIPLIER); } else if (_from >= bonusEndBlock) { return _to.sub(_from); } else { return bonusEndBlock.sub(_from).mul(BONUS_MULTIPLIER).add( _to.sub(bonusEndBlock) ); } } ``` Function call of `getMultiplier()` may return a wrong result if inputted block number, `_from`, is smaller than `startBlock`. This does NOT affect the core functions because they only use block numbers that are bigger than `startBlock` when calling function `getMultiplier()`. But wrong return values of function `getMultiplier()` may still misguide users.
Open the reportThe linked error messages use `COMP` as the project name, which is fork leftover. ```solidity=554 require(signatory != address(0), "Comp::delegateBySig: invalid signature"); require(nonce == nonces[signatory]++, "Comp::delegateBySig: invalid nonce"); require(now <= expiry, "Comp::delegateBySig: signature expired"); ```
Open the reportThe linked conditional conducts a strict comparison between `value` and ChainToken balance. However, the check should be passed when `balanceOf(msg.sender)` = `value`.
Open the reportThe contract has 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 different compiler versions. This can lead to an 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.
Open the reportAttached through a shared contract address. Our incident data ends in Oct 2026.
29 ETH.
Source: De.Fi incident database.
| Address | Chain | Source code |
|---|---|---|
| 0x41c3…3ca90x41c37a4683d6a05adb31c39d71348a8403b13ca9 | Chain: Ethereum | 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="onyx-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