Filmio

CertiKFiles named in the report

CertiK reviewed Film.io (Filmio) on 17 Mar 2023: 33 findings, 5 critical; 29 marked fixed, 1 acknowledged, 3 open.

5 critical findings, fixed
Open the reportOpen Film.io’s security record

Report hosted on skynet.certik.com.

Trustblock does not review code.

Findings33

Statuses are the security firm’s own, from its report.

SeverityOpenAcknowledgedFixed
Critical005
High001
Medium011
Low3022
CriticalChecked Case Logic is IncorrectFixed

In function `unstaketokens()`, there is an attempt to check that `stakeid` input matches the recorded parameter `stakeid` for a struct corresponding to the `stakeid` in the `stake` arrays `stakeofaccounts` and `objectStakes`. In such a case, the goal is to either delete the corresponding information or update the recorded `quantity` parameter. However, there is no reason that the `stakeid` parameter at `stakeofaccounts[stakeid - 1].stakeid` or `objectStakes[stakeid - 1].stake.id` should be equal to the input `stakeid`, and in most cases where multiple users are staking multiple times and in between one another, this is likely false. This means this check in the if-block conditional statement will cause issues in the function logic. ### Scenario Say 1000 users have already staked so that storage variable `stakeindex` is currently value 1000. Now assume that a user, Alice, wishes to stake for the first time. Previously for Alice, her corresponding `stake` array, `stakeofaccounts = []`, is an empty array. Alice successfully calls `staketokens()` and now `stakeIndex = 1001`, and her `stakeId` will also be `1001`. However, Alice has only staked once, so she has one entry in her array `stakeofaccounts`: a `stake` struct with `stakeId` of 1001. Thus `stakeofaccounts[0].stakeid` is 1001 for Alice, while a check to `stakeofaccounts[stakeid -1].stakeid` is a check of the 1000th entry of this array. This entry does not exist for Alice, and will cause a revert. In this way, if more than one user attempts to stake with this contract, all but the first staker will be kept from unstaking because of the revert caused by this logical issue. The same issue occurs with `objectStakes` logic.

CriticalEvaluations can be overwritten by other users witFixed

Any rating entry in the mapping `projectEvaluation` can be overwritten with a new chosen rating by any user that has previously created their own evaluation through the logic in `modifyEvaluation()`. The function checks that an input `prj_id` matches the parameter in `projectLockDetails[prj_id]` and `projectEvaluation[prj_id], that `prj_id` is nonzero, that the input `que_id` matches the parameter `projectEvaluation[prj_id].que_id`, and finally that `userEvaluation[_msgSender()].account` is the `_msgSender()`. If two distinct users have used function `createEvaluation()` for two distinct `prj_id` values, then either can update the `projectEvaluation` mapping of the other's `prj_id` to their own chosen `rating` value.

CriticalFunction `getlockedAmount()` Does Not Sum `_user`Fixed

Function `getlockedAmount()` is used to calculate the `lockedAmount` for a sending address in the internal `_transfer()` function of the token. While this function uses the length of `userLocks[_user]` as an upper bound in a for loop, to sum up `lockedAmount`, there are several issues with the logic of this function. 1. The `lockDuration` and `lockAmount` at `lockDetails[i]` are used in the function logic, where `i` is the iteration number of the for-loop. This entry of the mapping `lockDetails` may or may not correspond to the `_user` address, so the resulting `lockedAmount` that is calculated in the for-loop may or may not be equal to the actual total amount locked for the `_user` address. The value can be less or more than `_user` address's actual total locked amount. This is a critical vulnerability, as it may allow users to transfer funds that are meant to be locked, it may keep users from transferring amounts meant to be unlocked, and it may result in a `lockedAmount` that is larger than `balanceOf(from)` in `_transfer()`, causing a revert due to underflow. 2. The for-loop includes the value `userLocks[_user].length` as an iteration value `i` even though `userLocks[_user][length]` is a not an entry of the array `userLocks[_user]`. The function does not revert in this case though, because, again, `lockDetails[i]` is read instead of getting the `lockId` value held at `userLocks[_user][i]`, and `lockDetails` is a mapping while `userLocks[_user]` is an array. These two issues together allow this function to execute inconsistently and cause critical issues with the locking logic of the contract ### Scenario As a simplified example, Alice and Bob are set up with a locked amount of tokens through the `lockUp()` function. Alice is given 10 tokens first, so she gets lockId = 1, and Bob is given two locked amounts over lockId = 2 (say amount is 10), and lockId = 3 (say amount is 11). When Alice calls public view function `getlockedAmount()` to see her total locked amount, the `userLocks[_user].length` is 1, so the for-loop will iterate through i = 0 and i = 1. For i = 0, `lockDetails[0].lockDuration` is 0, so nothing is counted (this will always be true). When i =1, assuming `lockDetails[1].lockDuration` is larger than the current timestamp, then `lockedAmount` is updated to be `lockedDetails[i].lockAmount`, which is 10. It happens that in this case, i = 1 matches Alice's lockId that she owns, so the return value is accurate. Now consider Bob's case. Again, assuming both his locks are still upheld for the current `block.timestamp`, assume he calls the `getlockedAmount()` function with his address. Then his value of `userLocks[_user].length` is 2, and the for-loop will iterate through i = 0, 1, 2. As before, `lockDetails[0]` will not be counted. However, `lockedAmount` will get `lockDetails[1].lockAmount` (amount is 10), and `lockDetails[2].lockAmount` (amount is again 10) added to it. The total returned `lockedAmount` will be 20, even though for Bob, it is supposed to be 21. This is because two of the `lockDetails[i].lockAmount` values (i = 0, and i =1) did not belong to Bob, resulting in a miscalculation.

CriticalIf user creates new evaluation, old evaluation inFixed

If a user calls `createEvaluation()` more than once on two different `prj_id` inputs, then the first correspondence for `userEvaluation` mapping will be overwritten with the second, losing the first `EvaluateDetails` struct correspondence. However, the `EvaluateDetails` struct in the `projectEvaluation` mapping will persist for the first `prj_id`.

CriticalUsers can create a `stakeid` by sending tokens toFixed

When users call the function `staketokens()`, they specify an input address `to` which can be any address they choose as long as it is not the one they are making the call from. The `staketokens()` function uses this address to send tokens from the `_msgSender()` to the target `to` address. This creates recorded stake information within the contract that can then be used in the function `unstaketokens()` to transfer tokens from the `escrow_account` to the `_msgSender()`, regardless of where the tokens were first transferred. A user can have a boundless number of their own accounts. So a user can input one of their own addresses for the `to` input, transfer this `quantity` of tokens from their first account to their other owned account, then immediately call `unstaketokens()` to have an equivalent amount of tokens transferred from the `escrow_account` to their original account. Now they own twice as much of the token as they originally did across two accounts. This pattern can be repeated by the attacker until they have drained all available funds from the escrow account.

HighInitial Token DistributionFixed

All `FAN` tokens are sent to the `_msgSender()` address when initializing the contract. This is a potential centralization risk as the deployer can distribute all `FAN` tokens without the consensus of the community.

MediumInconsistent Rating RequirementsFixed

Function `createEvaluation()` has the following requirement: ```solidity require(rating>=100 && rating <=2000,"Rating should be between 100 to 2000"); ``` while function `modifyEvaluation()` has the requirement: ```solidity require(rating >= 10 && rating <= 2000,"Rating should be between 10 to 2000"); //rating is between 1 t0 20 ``` Based on the comment made next to the latter requirement, the rating should be scaled down by a factor of 100 to be 1 to 20, meaning that the rating requirement in function `createEvaluation()` is correct, while the rating requirement in `modifyEvaluation()` does not fit accurately reflect the scale. Any updates in `modifyEvaluation()` below the value of 100 may be incorrectly converted to a value of 0.

Low`escrow_account` can be made publicFixed

Since the `escrow_account` is the funding account address for unstaking in the `StakeV1` contract, the address should be made public, so that users can see the balance of this address and the allowances of the `StakeV1` address for the `escrow_account`. That way, they are able to make an informed decision about whether to add their tokens to the `StakeV1` contract.

LowCheck Effect Interaction Pattern ViolatedFixed

The order of external call/transfer and storage manipulation must follow the check-effect-interaction pattern. To follow this pattern, each call to `tokenInterface.transferFrom()` should occur at the very end of the function in which it is used.

LowComment in check on `userrating` check is InconsiFixed

In the function `createRating()` the input `userrating` must be strictly greater than 0 and no more than 50. The comment in the requirement states "Rating needs to be between 1 to 5". Considering the `userrating` is allowed to be as high as 50, it seems that `userrating` is supposed to be scaled by a factor of 10. However, any inputs for `userrating` that are lower than 10 in that case will be read as 0 when the value is scaled down, due to truncating decimals.

Show 10 more
LowDiscussion on Staking FunctionalityFixed

The contract `StakeV1` has only two functions that users are meant to interact with: `staketokens()` and `unstaketokens()`. Neither function contains logic for giving a user rewards for staking their tokens. Instead, when users call `unstaketokens()`, they are transferred the amount they were supposed to have originally staked, and nothing more.

LowFunction `addReview()` can be called more than onFixed

Function `addReview()` can be called more than one time, even though `reviewGiven` is already set to true.

LowInheriting from a Non-Upgradeable Contract in anFixed

Contract `ERC2771Recipient` is not an upgradeable contract and is not set up for the potential of adding extra storage variables. Usually parent contracts include a `__gap` placeholder storage variable to take up a requisite number of storage slots so that in the event new storage variables need to be added to the parent contract, they can be placed in one of the storage slots held by the `__gap` variable. If storage variables are added directly to contract `ERC2771Recipient` without the prior use of a `__gap`, then storage collisions can occur with the other storage variables, within the proxy contract. Likewise, contracts `VariableDataV1` and `FanTokenVariableV1` are not upgradeable contracts as constructed.

LowMissing Check `stakeid` is NonzeroFixed

While it is not possible for a user address to be associated to a `stakeid` of 0, if a user accidentally enters 0 as input for `stakeid`, the function `unstaketokens()` will revert and the user may not know why.

LowMissing Check for `_amount` in `lockUp()`Fixed

There is no check that `_amount` is nonzero in function `lockUp()`.

LowMissing Emit EventsFixed

Functions that update state variables and that are controlled by centralization roles should emit relevant events as notifications.

LowMissing InheritanceFixed

`FilmioVestingWallet` implements the interface `IFilmioVestingWallet`, but does not inherit from it. ```solidity=22 contract FilmioVestingWallet is Context, Initializable { ``` ```solidity=16 interface IFilmioVestingWallet { ```

LowMissing Zero Address ValidationFixed

The cited address input is missing a check that it is not `address(0)`.

LowName of Contract Does Not Match Project NameFixed

The project is called `Filmio` but the contract is name `FilmoProjectV1`

LowNo check that target address `_to` is not `addresFixed

If `_to` is `address(this)` in function `lockUp()`, then this will prevent the further use of function `lockUp()` until the specified time has passed.

Show 9 more
LowOnly One User Can create an evaluation for a giveFixed

Once a user calls `createEvaluation()` on a specific `prj_id` successfully for the first time, then `projectEvaluation` mapping gets updated for the `prj_id`, including updating the `account` parameter to the current `_msgSender()`. This means that if someone tries to call `createEvaluation()` using the same `prj_id` a second time, then the call will revert because of the requirement: ```solidity require(projectEvaluation[prj_id].account == address(0) && que_id != 0, "Project is already evaluated by user"); ``` Is it intended to only let a project corresponding to a `prj_id` be evaluated once on a first-come-first-serve basis?

LowRecommended Use of `IERC20Upgradeable`Fixed

Use libraries and interfaces specifically designed to be included with OpenZeppelin's upgradeable contracts to ensure compatibility with upgradeable-contract security patterns.

LowRedundant `import` StatementsFixed

The contract `VestingWallet.sol` is importing `Address.sol` and `Math.sol` from OpenZeppelin. \ `Address` is already imported through the contract `Initializable.sol` and `Math.sol` is never used.

LowToo Many DigitsFixed

Literals with many digits are difficult to read and review. ```solidity=16 _mint(_msgSender(), 100000000 * (10**decimals())); ```

LowUnchecked ERC-20 `transfer()`/`transferFrom()` CaFixed

The return value of the transfer()/transferFrom() call is not checked. ```solidity=124 tokenInterface.transferFrom(_msgSender(), to, quantity); ``` --- ```solidity=164 tokenInterface.transferFrom(escrow_account, _msgSender(), quantity); ```

LowUnprotected InitializerFixed

One or more logic contracts do not protect their initializers. An attacker can call the initializer and assume ownership of the logic contract, whereby she can perform privileged operations that trick unsuspecting users into believing that she is the owner of the upgradeable contract. ```solidity=7 contract FanTokenV1 is FanTokenVariableV1, ERC2771Recipient { ``` - `FanTokenV1` is an upgradeable contract that does not protect its initializer. ```solidity=12 function initialize(address _trustedForwarder) public initializer { ``` - `initialize` is an unprotected initializer function. --- ```solidity=8 contract FilmoProjectV1 is VariableDataV1, ERC2771Recipient { ``` - `FilmoProjectV1` is an upgradeable contract that does not protect its initializer. ```solidity=12 function initialize(address _trustedForwarder) public initializer { ``` - `initialize` is an unprotected initializer function. --- ```solidity=9 contract StakeV1 is ReentrancyGuardUpgradeable, ERC2771Recipient { ``` - `StakeV1` is an upgradeable contract that does not protect its initializer. ```solidity=57 function initialize(address token, address escrowaccount, address _trustedForwarder) public initializer { ``` - `initialize` is an unprotected initializer function.

LowUnverified User-Provided InputsFixed

The following user-provided input is not verified for potentially important state variables. - For contract `StakeV1`, the user-provided `typeid` is stored without checking that it matches the rest of the `stake` information. A similar issue occurs with the string input `staketype`. - For contract `FilmoProjectV1`, the user-provided `que_id` is stored without checking that this information correctly corresponds to the user's other inputs. - For contract `FilmoProjectV1`, the privileged account `owner` provides uint array `user_id` as input which is stored in a `LockDetails` struct and never used again within the contract. It is unclear how the unsigned integer values are meant to reference users, and what this information is meant to be used on. The concern is that if this information is used by the frontend as a reference to make further decisions in logic, a user can provide any value they want for these inputs, and manipulate the outcome. Moreover, there is no check that these values make sense for the project. For instance, the user can enter any string value they want for `staketype`, which may have nothing to do with the actual stake type. In `FilmoProjectV1`, we speculate that the `user_id` array was originally supposed to be used to check that only specific addresses are allowed to call the functions of this contract, but no such check exists.

LowUse of Storage VariablesFixed

There are two concerns with the use of storage variables in function `unstaketokens()`. ### Use of `stakedetails` vs Directly Updating `stakeDetails` mapping In line 149, a reference variable, `stakedetails`, is created which directly references the storage variable `stakeDetails[stakeid]`; the use of the qualifier `storage` means that any time `stakedetails` is updated in the function, it actually updates the `stake` struct `stakeDetails[stakeid]` in storage. Despite this, the `stakedetails` reference is only used to read from storage. Instead, a secondary variable `currentstake` is created and located in memory, at line 181, which is then updated and used to directly change `stakeDetails[stakeid]`. This kind of inconsistency makes the code difficult to check. At best, the creation of storage reference `stakedetails` is unnecessary and can just be located in memory, so that a secondary `currentstake` does not need to be created. We recommend reviewing the above and taking steps to streamline the references to storage variables, both to make it easier to assess and to avoid potential inconsistencies in updates. ### Double-Update of `stake` arrays Both `objectStakeDetails[stakedetails.staketype]` and `accountStakeDetails[stakedetails.account]` are held in reference variables, in lines 166 and 167 respectively, as `objectStakes` and `stakeofaccounts`. Again, any changes to these variables update state. In lines 171 and 173, this updates the same information twice, when the if-block condition is met. When the if-block condition is not met, the information for `accountStakeDetail[stakedetails.account]` is updated to be the exact information it already was previously. The same is true of `objectStakeDetails[stakedetails.staketype]` in lines 175 and 177. The same kind of issue occurs in lines 185, 186, 188, and 189. In other words, the secondary updates at lines 173, 177, 186, and 189 are unnecessary.

LowVariables That Could Be Declared as ImmutableFixed

The linked variables assigned in the constructor can be declared as `immutable`. Immutable state variables can be assigned during contract creation but will remain constant throughout the lifetime of a deployed contract. A big advantage of immutable variables is that reading them is significantly cheaper than reading from regular state variables since they will not be stored in storage.

Contracts in scope

No deployed address is recorded for this audit.

The report names a repository or files. Open the report to see its scope.

Repository and files the report names (9)

  • VestingContracts/contracts/VestingWallet/IVestingWallet.sol
  • Project/contracts/FilmoProjectV1.sol
  • Stake/contracts/StakeV1.sol
  • FanToken/contracts/ERC20Upgradeable.sol
  • VestingContracts/contracts/VestingWalletFactory.sol
  • FanToken/contracts/FanTokenVariableV1.sol
  • FanToken/contracts/FanERC20TokenV1.sol
  • Project/contracts/VariableDataV1.sol
  • VestingContracts/contracts/VestingWallet/VestingWallet.sol

Part of Film.io’s security record

Film.io
Audits1 audit by 1 firmLast audit Mar 2023, by CertiK.
Critical findingsNo open critical findingsIn that report, 5 critical findings were reported. Each is marked fixed by the firm that reported it.
Open Film.io’s security record

Security firm

CertiK

1,044 projects reviewed · 1,375 reports · latest May 2025

See CertiK’s audits

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