CertiK reviewed Ordify on 22 Mar 2024: 29 findings, 0 critical; 16 marked fixed, 2 acknowledged, 11 open.
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 | 0 |
| Medium | 1 | 0 | 2 |
| Low | 10 | 0 | 14 |
The `createVestings` function can be invoked by the contract owner to create a vesting plan for a specific user, and when `depositCheck` is enabled, the contract will ensure there is enough token allocated in the contract. ```solidity=161 uint256 totalTokenAvailable = totalDepositedAmount + totalAmountClaimed - totalAmountAllocated; ``` The user is allowed to drop vesting via `requestRefund`, as a result, the vesting amount will be added to the ```solidity=226 totalAmountRefundRequested += vesting.amount; ``` However, the `requestRefund` will not update the `totalAmountAllocated`, once the contract owner `withdrawRequestRefundToken` from the contract. There might not be enough tokens for new vesting, because the canceled vesting is still part of the `totalAmountAllocated`, and the contract balance needs to cover it.
After the event finished, there are two conditions that user can refund: 1. User has a valid signature from `verifyingAddressECDSA`. 2. `globalRefundEnabled` is set with `true`. AAlso, the actual refund sum is calculated with two phases contributions and `refundPercentage`: ```solidity=160 uint256 userContributed = userDetailsRecord.contributedRound1 + userDetailsRecord.contributedRound2; uint256 refundCalculationDividend = userContributed * refundPercentage; userRefoundAmount = refundCalculationDividend / (BASIS_POINT_RATE_CONVERTER); ``` The `refundPercentage` can be set with: - `setRefundPercentage` - `prepareForGlobalRefund` However, the `prepareForGlobalRefund` will avoid the `refundPercentage` to be 0, but `setRefundPercentage` can set 0 `refundPercentage`, which is inconsistent on design that ``refundPercentage` > 0`. And if `refundPercentage` is 0 user cannot get any refund.
In the latest version of the code, a new function called `emergencyWithdrawWithoutReward` has been introduced. This function is designed to prevent the user's deposited assets from being locked up in the event that the pool lacks sufficient rewards. However, this update has led to some concerns: 1. The validation check for `prematureWithdrawEnabled` within the `withdraw` function can be circumvented by users through a combination of `claim` and `emergencyWithdrawWithoutReward` calls. This means that even if `prematureWithdrawEnabled` is set to false, indicating that early withdrawal is not allowed, users can still withdraw their stakes prematurely. We would like to check with the team if such scenario is considered. 2. When using the `emergencyWithdrawWithoutReward` function, the system does not record any reward information for the stake. As a result, any rewards that might be allocated to the user in the future will be permanently lost, as there is no record of these rewards being associated with the user's stake.
According to the code, whenever a claim is made, the contract will transfer token to the user and accumulate `claimableAmount` to `totalAmountClaimed`. However, in the function `createVesting()`, when `airdroptedAtStart` is true, `totalAmountClaimed` will be directly modified without transferring money to the user.\
Based on the updated codebase, the `round2Multiplier` is no longer used for round2 allowance calculation, and no other logic is attached to it. As a result, the `round2Multiplier` and corresponding setup logic are redundant.
The `Helper` function will store tiers configuration to classify users' staking status. When deploying the contract, the following code snippets are implemented to update the `tiers` and `tiersMap`: ```solidity Tier memory noneTier = Tier("NONE", 0, 0); tiers.push(noneTier); tiersMap["NONE"] = noneTier; Tier memory prevTier = tiers[0]; for (uint256 i = 0; i < _tiers.length; i++) { require(prevTier.amountNeeded < _tiers[i].amountNeeded, "Tiers must be oredered amount needed"); tiers.push(_tiers[i]); tiersMap[_tiers[i].name] = _tiers[i]; prevTier = _tiers[i]; } ``` However, there is no validation if `_tiers[i].name` is already filled, as a result, duplicate records may exist with the same name and previous data stored in the given name will be overwritten.
Based on the function `withdrawRefundForAll()`, it can be inferred that this function is intended to execute the withdrawal of refund assets only when the refund mode is enabled. Therefore, when refundMode is true, "refund mode is on"; conversely, when refundMode is false, "refund mode" is off.
The following function is mainly used to withdraw penalty refund, however, the function should be `10000 - refundPercentage` based on the implementation: ```solidity=278 /** * @dev method used to refund stable token after refund is activated. Only part of refund is available. 1000 - refundPercentage */ function withdrawStableRefund() external onlyOwner { ```
When deploying the contract, it was checked whether the values of `withdrawFeePercentage` and `rate` were within the valid range, but the lack of a check here may cause the setting to be outside the expected or valid range. ```solidity=175 require(_stakingDefinition.withdrawFeePercentage <= 100, "withdraw perct too big"); require(_stakingDefinition.rate <= 1000, "rate too big"); ```
The cited address(`treasury`) input is missing a check that it is not `address(0)`.
Signed messages do not include the current chain ID, thus allowing attackers to perform replay attacks across chains when the launchpad are deployed on the other chains with the same address. ```solidity=121 bytes32 signedMessageHash = keccak256(abi.encode(address(this), user)).toEthSignedMessageHash(); ``` ```solidity=473 bytes32 signedMessageHash = keccak256(abi.encode(address(this), user, whitelistedAmount)).toEthSignedMessageHash(); ```
The `createVestings` function is being used to create vesting for the user. And the user reserves the grace period to request the refund via `requestRefund` function. ```solidity=223 require(block.timestamp <= (startDateAt + refundGracePeriodDuration), "refund period passed"); ``` However, since there is no time restriction on `createVestings`, the user may not always allow refunding the vesting.
Based on the comments in Helper contract, the weights for tiers are for IDO snapshot purposes (which is not in the current scope). And based on the test, the weight for the previous tier should always be less than the following tiers, similar to the `amountNeeded` value. However, such validation is not implemented in the current codebase.
In the withdraw function, it is already checked whether the `rewardBalance` of the contract is greater than the user reward. ```solidity=431 require(rewardBalance >= reward, "rewardBalance is too low"); ``` ```solidity=439 require(rewardBalance >= reward, "rewardBalance less than reward"); ```
The new mapping `tiersInitialization` is adding in the Helper contract in the updated codebase. However, this mapping is never used.
In the function `_buy()`, the `stablecoin.safeTransferFrom` is performed before updates to contract states. In the function `stake()`, the `tokenAddress.safeTransferFrom` is performed before updates to state(user deposit). In the function `addReward()`, the `tokenAddress.safeTransferFrom` is is performed before updates to state(reward balance).
The report names a repository or files. Open the report to see its scope.
Repository and files the report names (5)
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