Quantstamp reviewed LiquiFi on 27 Jan 2022: 21 findings, 0 critical; 18 marked fixed, 1 acknowledged, 2 open.
Report hosted on certificate.quantstamp.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 | 0 | 2 |
| Medium | 0 | 1 | 1 |
| Low | 2 | 0 | 15 |
The function accepts a parameter called `_totalSupply` but does not utilize it properly. It neither checks whether `_cadenceSeconds * _cadenceAmount` is less than the `_totalSupply`, nor whether there are rounding issues that will lead to lost tokens. In case the company owner made a mistake with the inputs, the tokens might vest a lot faster than intended or require manual intervention of the contract owner to retrieve the locked tokens.
The contract does not have any functionality to allow a company to stop vesting a former employee. There are two kinds of employees, `activeEmployees` and `inactiveEmployees`. The contract keeps track of them but never utilizes this information in regard to enabling/disabling of vesting.
Functions `Escrow.deposit()` and `Escrow.withdraw()` are using the _unsafe_ ERC20 interface functions of OpenZeppelin (`transferFrom()` and `transfer()`). Since [some ERC20 tokens do not return a value for these functions](https://medium.com/coinmonks/missing-return-value-bug-at-least-130-tokens-affected-d67bf08521ca) these calls might revert.
The `createPlan` function in the modules contracts does not check whether a plan with the provided ID already exists, allowing the owner to overwrite any existing plan.
According to the comments, the function `EmployeeRegistry.isOwner()` should return `true` only if the provided address parameter is the owner of of the provided company id parameter and `false` otherwise. However, as the `exists` return value of `EnumerableMap.tryGet(companyOwners, _companyId)` is not checked, providing `address(0)` to any company id would lead to returning `owner == _employee` equal to `true` and therefore violating the comments assumption.
There are two issues: 1. The function `transferOwner` does not check if `_newOwner` is an employee of the `_companyId`. The `_newOwner` could be an inactive employee, or even not an employee of that company. This would violate the assumption that a company owner should be an active employee of that company. 1. The function `transferOwner` does not check if `_newOwner` is not equal to `0x0`. This could lead to a disastrous result: A company has no owner to modify its data.
Although `Escrow.sol` inherits from `ReentrancyGuardUpgradeable`, it does not make use of the `nonReentrant` modifier. Further, functions `deposit()` and `withdraw()` do not comply with the ["Checks-Effects-Interactions" pattern](https://docs.soliditylang.org/en/v0.8.11/security-considerations.html#use-the-checks-effects-interactions-pattern), by performing state variable changes **after** external function calls and not before (`escrowBalance[][]` being changed after calling `transferFrom()`, `balanceOf()` and `transfer()`). As these lower level functions are only callable by `owner` or `vestingControllerAddress` the potential impact is limited.
The functions `createVestPlan` and `vest` are declared `payable`, allowing ether to be sent along the function calls. Since there is no function to retrieve the ether, it will lead to locked funds.
When smart contracts are deployed or their associated functions are invoked, the execution of these operations always consumes a certain quantity of gas, according to the amount of computation required to accomplish them. Modifying an unknown-size array that grows in size over time can result in a Denial-of-Service attack. Simply by having an excessively huge array, users can exceed the gas limit, therefore preventing the transaction from ever succeeding. For the loop on `L242`, the code does not restrict the number of vest plans that an employee in a company can have. This function might run out of gas and cannot be recovered from that state when the employee has too many vest plans. This could happen in case the company owner wants to deny an employee from accessing their vested tokens by adding many low-value vest plans.
The function `EmployeeRegistry.addEmployee()` does not check the return value of the call `EnumerableSet.add(companyIdToEmployees[_companyId].activeEmployees, _employee);`, which would indicate whether the added employee was already in the set. Therefore, calling the function multiple times would lead to duplicate company id entries in `employeeToCompanyIds` after executing `employeeToCompanyIds[_employee].push(_companyId);`.
Some contracts implement the `IVest` or `IModule` interface without explicitly inheriting from them. This can lead to mismatched function signatures as is the case here. Some of the functions are missing the `view` specifier, none of them specify a return type and none of the modules implement a `getPlan` function.
It is important to validate inputs, even if they only come from trusted addresses in order to avoid human error. The following functions do not have proper validation of input parameters: - `Escrow.initialize` - `Vesting.initialize` - `{Escrow,CliffModule,LinearModule,StreamModule}.setVestingControllerAddress`
Not all of the initializers from the inherited contracts are called. While this might not lead to issues with the current version, this can change in the future.
The `EmployeeRegistry` contract has several functions related to company ownership that are simply referred to as `owner`. Since the contract also inherits from `Ownable`, this might lead to confusion.
The `EmployeeRegistry` contract does not employ any access control for adding or archiving employees, enabling anyone to add arbitrary addresses as *active employees* to arbitrary company ids or mark them as inactive. As a result, the distinction between active and inactive employees is currently pointless.
There are several member variables that contain identical values, one typed and one `address`. This is redundant since the address can be accessed by casting it like this: `address(cliffModule)`. List of redundant variables: ```solidity address public employeeRegistryAddress; address public cliffModuleAddress; address public linearModuleAddress; address public realtimeModuleAddress; address public escrowAddress; ```
Starting from solidity version 0.8, overflows and underflows revert by default (see [docs](https://docs.soliditylang.org/en/v0.8.11/080-breaking-changes.html#silent-changes-of-the-semantics)), rendering the use of SafeMath unnecessary.
The `getVestingControllerAddress` view function has the `onlyOwner` modifier. Since the `vestingControllerAddress` variable is public anyway, this serves no purpose.
The report names a repository or files. Open the report to see its scope.
Repository and files the report names (8)
236 projects reviewed · 332 reports · latest Oct 2026
See Quantstamp’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