As of 21 Oct 2026, Illuvium has 6 audit reports by 4 security firms on Trustblock; the latest is by Cyfrin, dated 21 Oct 2025. Those reports list no open critical findings.
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 |
|---|---|---|---|---|---|
| Illuvium Staking V3 Audit Report | Security firm: Cyfrin | Date: | Scope: Not recorded (see the report) | No open critical findings | Open · GitHub |
| Illuvium Land Sale Protocol | Security firm: CertiK | Date: | Scope: Files named in the report | No open critical findings | Open · skynet.certik.com |
| Illuvium audit | Security firm: PeckShield | Date: | Scope: Not recorded (see the report) | No open critical findings | Open · GitHub |
| Illuvium (Part 4 - Staking V2) | Security firm: Quantstamp | Date: | Scope: Files named in the report | No open critical findings | Open · certificate.quantstamp.com |
| Illuvium Yield Farming Rewards | Security firm: Quantstamp | Date: | Scope: Files named in the report | No open critical findings | Open · certificate.quantstamp.com |
| Illuvium ERC20 | Security firm: Quantstamp | Date: | Scope: Files named in the report | 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 | 0 | 2 | 2 |
| Medium | 2 | 0 | 5 |
| Low | 2 | 10 | 37 |
The comment on the following line is not implemented; only one value (`subVaultRewards `) is updated. ``` // subYieldRewards and subVaultRewards needs to be updated on every `_processRewards` call user.subVaultRewards = uint256(user.totalWeight).weightToReward(vaultRewardsPerWeight); ```
Open the reportThe function `swapEthForIlv` uses Uniswap to exchange ETH for ILV tokens. The function is declared public with no access control. If the contract holds a large amount of ETH, attackers can manipulate (likely using flash loans) the Uniswap ETH/ILV price such that the `IlluviumVault` will receive an unfavorable amount of ILV. Further, computing `ilvOut` in the function leaves it susceptible to [sandwich attacks](https://medium.com/coinmonks/demystify-the-dark-forest-on-ethereum-sandwich-attacks-5a3aec9fa33e).
Open the reportThe report gives no description here.
Open the reportThe function `stake` calls `transferFrom` but ignores the return value; function `unstake` calls `transfer` and also ignores the return value.
Open the reportIn `sendILVRewards`, there are approvals with `type(uint256).max` which is dangerous if something goes wrong.
Open the reportThe `_processVaultRewards` function inside `IlluviumCorePool` and in `IlluviumLockedPool` will not give users the full amount of the reward they are entitled to, when `pendingVaultClaim > poolTokenReserve`. Moreover, the function will also stop the users from requesting for the missing amount afterward. Hence the users will lose rewards.
Open the reportThe `IlluviumPoolBase._updateStakeLock` function does not flush rewards before changing the value of `user.totalWeight`. This may lead to incorrect reward amounts subsequently.
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 reportIn the function `initialize()`, when the sale is in paused state, the value of `_pauseDuration` will be incorrectly emitted in the `Resumed` event.
Open the reportThe oracle update timeframe is hardcoded as 30 days, which lacks of readability and maintenance
Open the reportin `setMetadata()`, there are no input validations on `Site Type` and `Landmark Type ID`, where - `Site Type` must be in the range of [1, 6] - `Landmark Type ID` must be in the range of [0, 7]
Open the reportThere should always be events emitted in the sensitive functions that are controlled by centralization roles.
Open the reportThe **require** can be used to check for conditions and throw an exception if the condition is not met. It is better to provide a string message containing details about the error that will be passed back to the caller.
Open the reportWhile the ERC-20 implementation does necessitate that the `transferFrom()` / `transfer()` function returns a `bool` variable yielding `true`, many token implementations do not return anything i.e. Tether (USDT) leading to unexpected halts in code execution.
Open the reportThe report gives no description here.
Open the reportThe report gives no description here.
Open the reportThe function `migrateUser(...)` contains the following: `emit LogMigrateUser(msg.sender, _to)`. Perhaps adding more information to this log would help in future investigations.
Open the reportThe functions from this file can be called by anyone. However, the code comment states that “External `MigrateLockedStakes` call, [is] used in Sushi LP pool”.
Open the reportIn `CorePool.sol`, lines 274 and 275 `paramIndex` for `_initTime` and `_weight` are off-by-one. Similarly, line 40 in `VaultRecipient.sol` has an incorrect `paramIndex`.
Open the reportIn `ILVPool.sol`, the hash comment `bytes4(keccak256("_migrateYieldWeights(bytes32[],uint256,uint256")))` in function `_migrateYieldWeights` does not have the closing bracket for `_migrateYieldWeights` inside the quote. In `CorePool.sol`, the hash `0x867a0347` is not explained when it should be.
Open the reportIn `getV1StakePosition`, the value zero is returned if the stake id provided is not found. However, this value is also returned if the stake is the first entry.
Open the reportAll files that inherit from the `Ownable` contract could be left with no owner if the latter renounces his ownership. As one potential consequence, that could prevents update to the deployed contracts, requiring re-deploys and potential downtime.
Open the reportThe function `migrateUser(...)` is related to migrations of wallets of the same user, and not migrating users from V1 to V2. Thus, we encourage the team to provide a better name for this function.
Open the reportThe proper declaration of the visibility of state variables is a good programming practice and it also reduces the final contract size. Public variables increase the contract size:for each public variable, the solidity compiler automatically generates the `getter` and `setter`[functions](https://docs.soliditylang.org/en/v0.6.10/contracts.html#visibility-and-getters). For instance, we do not see a reason for declaring the following state variables as `public` in `Vault.sol`: - `Pools public pools;` - `IUniswapV2Router02 public sushiRouter` and - `IERC20 public ilv`.
Open the reportThe functions `migrateUser(...)`, `sync()`, `receiveVaultRewards(...)`, `setWeight(...)`, and `pendingRewards(...)`can be called when the contract is paused. This can be dangerous since they change state variables.
Open the reportThe contract size can be optimized by turning `modifiers` into `functions`, as shown in the example below: ``` modifier checkStuff() {} function doSomething() checkStuff {} ``` The `function doSomething()` can rewritten without modifiers as: ``` function checkStuff() private {} function doSomething() { checkStuff(); } ```
Open the reportIn `setEndTime`, there is no sanity checks for the new value. It could be before the current time, or interact with existing pools in unexpected ways.
Open the reportThe function `unstake` in `FlashPool.sol` converts a `uint256` to a `unit128` without checking that the cast is safe. Similar issues - with as small a bit width as 64 - exist elsewhere in the contract. These issues are found in `CorePool.sol` as well.
Open the reportIf the ETH balance of `IlluviumVault` is zero (possibly due to a previous call to either `swapEthForIlv` or `sendIlvRewards`), the function will revert due to the check `balance > 0` on L159. However, since `swapEthForIlv` is public, a legitimate call to `sendIlvRewards` could be griefed by any user if they front-run with a call to `swapEthForIlv`.
Open the reportAccording to the inline documentation, step 2 should invoke `setPool`, and step 3 sets balances (through potentially multiple calls to `setBalances`). However, the function `setBalances` requires on L164 that `address(pool) == address(0)`, so step 3 cannot occur after step 2. The steps on L17-20 appear correct, but L126 and L149 do not align with this summary.
Open the reportIn the `IlluviumCorePool.stakeAsPool` function on L155 code comment says that the `_useSILV` should be `false`, however it's not in the case on L165 where the 2nd parameter passed to the `_processRewards` function, which represents the value of `_useSILV` is hardcoded to `true`.
Open the reportAssumptions about intermediate values during function processing should be explicitly checked, especially if these values depend on outputs returned by external contract calls. For example, we assume that the value of `stakeWeight` on L420 inside of `IlluviumPoolBase._stake()` should be greater than zero. Otherwise, it doesn't make sense to create a deposit with `stakeWeight == 0`. <br><br> Note that this is one example of what we assume to be an implicit assumption, however, all implicit assumptions should be checked in a similar way.
Open the reportThe following instances of missing or insufficient input validation have been encountered: 1. The `_pool` parameter of the of the `TokenLocking.setPool()` function is not checked to conform to the `ILockedPool` interface and could be any address. 2. The `_rewardsAmount` parameter of the `IlluviumLockedPool.receiveVaultRewards()` function is not checked to be greater than zero. The same applies to the function with the same name in other contracts. 3. The `_from` and `_to` parameters of `IlluviumLockedPool.changeLockedHolder()` are not checked to be different. This could lead to deleting a holder. 4. The `_vault` parameter of `IlluviumCorePool.setVault()` is not checked to be different from `address(0)`. 5. The `_weight` parameter of `IlluviumPoolBase.setWeight()` is not checked to be greater than zero. Note that this is not an exhaustive list. User inputs should always be validated.
Open the reportThe `TokenLocking.setBalances()` function may be called multiple times in order to "allow setting balance to zero in case of accidental addition of the holder". However, due to missing checks/assertions there exists a possibility for human error which could lead to unlocking a total balance larger than intended.
Open the reportIn `IlluviumCorePool.sol` on L30, the comment states that `poolTokenReserve` is the "Total value of ILV tokens available in the pool". However, while functions such as `IlluviumCorePool.receiveVaultRewards` only increase `poolTokenReserve` when `poolToken == ilv`, this is not the case for functions such as `IlluviumCorePool._stake`. For example, if the `poolToken` is `ILV/ETH Pair`, the `poolTokenReserve` is still increased on L205, even though pair tokens are staked (not ILV). This makes the computation in `IlluviumVault.sendIlvRewards` of `ilvInPairPool` unclear: ``` uint256 ilvInPairPool = (pairPoolReserve.mul(ilv.balanceOf(address(ilvEthPair))).div(ilvEthPair.totalSupply())).add( pairPoolIlvBalance ); ``` In particular, the expression `(pairPoolReserve.mul(ilv.balanceOf(address(ilvEthPair))).div(ilvEthPair.totalSupply()))` seems to suggest that `pairPoolReserve` should store the amount of `ILV/ETH Pair` tokens in the `ILV/ETH Pair` pool, *NOT* the ILV balance itself. With the current setup, it appears that the `ilvInPairPool` computation above will double-count some tokens, since `pairPoolReserve` is increased for both `ILV` and `ILV/ETH Pair` deposits. This will inflate the weight associated with the `ILV/ETH Pair` pool.
Open the reportThe `now256()` and `blockNumber()` functions declared on L235 and L224 in `IlluviumLockedPool.sol` are never used.
Open the report`FeaturesUpdated` event defined in `AccessControl.sol` contract is never used. Instead, the `RoleUpdated` event is emitted when features are updated.
Open the reportSignature expiration should occur at the block with timestamp specified by the `_exp` input parameter of the `delegateWithSig()` function. This means that at `block.timestamp == _exp` the signature should already be expired. However, the `require` statement on L1014 only throws if `block.timestamp > _exp`.
Open the reportIn L742-745, the checks are wrong. The check in L742 tests for overflow (instead of underflow), whereas the check in L745 allows to decrease by 0 (which contradicts the documentation).
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="illuvium"></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