CertiK reviewed Solidus Ai Tech (Vision Makers) on 12 Feb 2025: 16 findings, 0 critical; 10 marked fixed, 6 acknowledged.
Report hosted on skynet.certik.com.
Trustblock does not review code.
Statuses are the security firm’s own, from its report.
| Severity | Open | Acknowledged | Fixed |
|---|---|---|---|
| Critical | 0 | 0 | 0 |
| High | 0 | 2 | 1 |
| Medium | 0 | 0 | 4 |
| Low | 0 | 4 | 5 |
The owner has privilege to withdraw any token in the `LinearVestingPool` contract and transfer it to any `recipient` address. If a malicious actor got control of an address with the MANAGER_ROLE, entire token balance from the `LinearVestingPool` contract can be drained and users would not be able to claim any vested tokens. The MANAGER_ROLE also has the authority to change `token` address via the `changeToken()` function of the `PoolManager` contract, which could potentially cause users unable to withdraw tokens from the `LinearVestingPool` contract. Additionally, all the fees can be up to the `PERCENTAGE_DENOMINATOR`. When the refund fee equals the `PERCENTAGE_DENOMINATOR`, users would receive 0 when they call the `refund()` function.
The `claimFunds()` function of the `InvestmentPool` contract contains a `vesting.rescueTokens(projectFounder, tokensToReturn)` call. In the `LinearVestingPool` contract, the `rescueTokens()` function has a `onlyPoolManager` modifier, which restricts the caller to the `PoolManager` contract. As such, when the `InvestmentPool` contract calls `rescueTokens()`, the transaction would revert, which could make the project founder unable to call the `claimFunds()` function.
When users invest `paymentToken` into the `InvestmentPool` contract, the amount of token to be sold/vested is calculated as `totalTokenAmount = (investedAmount * totalTokensOnSale) / totalAllocation`. However, when users withdraw the vested tokens from the `LinearVestingPool` contract, the same `withdrawableAmount` is used to transfer the token to the user and to release `paymentToken` to the `projectFounder`, and there's no ratio based on `totalTokensOnSale / totalAllocation` being utilized. Depending on the actual ratio of `totalTokensOnSale / totalAllocation`, this means that either too much or too little `paymentToken` is being released to the project founder when users withdraw vested tokens.
In the `initialize()` function of the `LinearVestingPool` contract, the `investmentPool` contract is set to be the `Owner` of the `LinearVestingPool` contract in line 149. The `revoke()`, `updateStreamStatus()`, and `updateStream()` functions all have the `onlyOwner` modifiers, which means only the `investmentPool` contract can call these functions. However, in the `investmentPool` contract, there's no mechanism to call these functions. Note that in the `LinearVestingPool.js` test file, line 34 sets the `deployer` to be the `Owner` instead of the `investmentPool` contract, which is inconsistent with the actual behavior of the `PoolManager` and the `LinearVestingPool` contracts.
The `updatePool()` function contains the `onlyPoolManager` modifier, but the `PoolManager` contract does not contain a method to call the `updatePool()` function of the `LinearVestingPool` contract, which means that this function cannot be called.
If an event is not indexed in a smart contract, it means that the event's parameters are not tagged with the `indexed` keyword. This has implications for how the event data can be searched and filtered when looking through blockchain logs. Without indexing, the event will still emit the data as part of the transaction log, but users won't be able to query for these events using the parameters. They'll have to retrieve the entire set of logs and manually sift through them to find events with the specific data. This can be less efficient and more time-consuming, especially on a blockchain with a high volume of transactions and events.
In the `PoolManager` contract, when new `vestingPool` and `investmentPool` contracts are deployed and initialized, there is parameter validation within each contract individually. However, there's no parameter validation between the `investment` and `vesting` parameters, such as the start time of investment and vesting. Incorrectly configured parameters could potentially cause unintended consequences.
When users call the `withdraw()` function, the `releaseFundsToProjectFounder()` function is only called when `stream.withdrawnAmount == 0`. If a user calls the `withdraw()` function when only a tiny fraction of token has vested, the function would only release a tiny amount of payment token to the project founder. When all the tokens are fully vested, no additional payment token would be released to project founder, as line 393 would not be `true` in subsequent calls to the `withdraw()` function. Furthermore, given that there's no check between the `msg.sender` and the provided `wallet` address, a malicious actor could use the trick above on all active streams even if they are not the corresponding `wallet` address.
When attempting to index dynamic data types like `string`, `bytes`, `array`, or `struct` in Solidity, they don't get stored in their original form. Instead, the Ethereum log system stores the `Keccak-256` hash of these data types. While this approach ensures efficiency and cost-effectiveness, developers must be aware of it to correctly use and interpret logs, and they cannot retrieve the original string from its hash alone.
The `claimFunds()` and `invest()` functions in the `InvestmentPool` contract contain a time range check based on the `endDate` variable. When `block.timestamp == endDate`, it satisfies the conditions of both functions.
The report names a repository or files. Open the report to see its scope.
Repository and files the report names (3)
1,044 projects reviewed · 1,375 reports · latest May 2025
See CertiK’s auditsSomething 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