CertiK reviewed Galactix Zone (Galactixzone) on 24 Nov 2022: 30 findings, 1 critical; 23 marked fixed, 7 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 | 1 |
| High | 3 | 0 | 1 |
| Medium | 1 | 0 | 2 |
| Low | 3 | 0 | 19 |
The string `mergeSet` created from function `generateMergeSet()` in library `MergeHelper` is not unique for a given `uint` array `_tokenIds`. For instance, the array `[1,2,3]` (an array of token Ids 1, 2, and 3) will yield string `123`, but so will array `[123]` (an array containing only token Id 123). Because of this, mapping `mergeSetCreatedAt` can be overwritten for two differing, legitimate inputs, with two differing signatures. This can result in permanently locked tokens that cannot be burned nor used to mint new tokens, since `mergeSetCreatedAt` is used as a check in the merge functions.
In contract `key`, function `verifySignature()` uses the NFT `balanceOf(_buyer)` in verifying a signature for given input information. Please explain the significance of the check on the balance. Once the balance is attained by the user, they need only maintain that balance to use the signature again. This can be done by either buying no new NFTs, or buying more and transferring to a secondary account. Is the intention to confirm that the user has bought a certain threshold amount of `GMA` NFTs in order to buy more, or is it to make sure they do not own a certain number? If it is the latter, this check will not prevent this scenario.
Functions `temporaryMerge()` and `permanentMerge()` achieve the same end result, but `temporaryMerge()` requires execution within four minutes of the `_timestamp` used in signature verification, while `permanentMerge()` does not. This appears to make this check ineffective, since a user can use the same signed information in either function successfully.
A user can bypass the ERC721 transfer fee. Function `safeTransferFrom(address from, address to, uint256 tokenId)` in the inherited `ERC721Upgradeable` contract calls to the other public function ```solidity safeTransferFrom( address from, address to, uint256 tokenId, bytes memory data ) ``` the only difference being the secondary function has an option for input `data`. Users can call this latter function directly, thus bypassing the transfer fee.
Contract `GalactixToken` inherits from contract `Ownable`. While the deployer is made the `owner` of the contract, the `onlyOwner` modifier is never used on any of the functionality.
The event emitted uses the inputs `_endTime`, `_priceInBUSD`, and `_priceAfter24Hours` regardless of which `_salePhase` is being deployed. However, in `_salePhase` 4, this information is meant to change.
In function `deployNewSalePhase()`, if `_salePhase` is 4, then `salePhaseStatistics[_salePhase].priceInBUSD` is updated to `launchPrice`. Regardless, it is also updated to input `_priceInBUSD` further in the function code. On a related note, `salePhaseStatistics[_salePhase].priceAfter24Hours` is set to the input value `_priceAfter24Hours`. This may lead to unexpected changes in the price if the intention is for the `priceInBUSD` for the fourth sale phase to stay at the launch price in perpetuity at that time.
If a user inputs `false` for `_isUsingBNB`, but sends BNB with the function call, the BNB will not be sent to the `treasuryAddress` and instead will be locked in the contract.
The function `initialize()` does not call `__ReentrancyGuard_init()`. This function initializes the `status` variable in the parent contract.
The given input is missing a check for the following: **Contract `dNFT`** - `_feeRate` is missing a check that it is no larger that 10000 (the dividing factor). - `_minimumGalactixTokenRequire` is missing a check that it is nonzero. - `_rescuePrice` is missing a check that it is nonzero. - `_launchPrice` is missing a check that it is nonzero. - `_claimableTime` is missing a check that it is nonzero. - function `executeTemporaryMerge()` is missing a check that the relevant session Id exists (it should revert if the session Id does not exist). - function `permanentMerge()` is missing a check on input `_sessionId` and `_timestamp`. --- **Contract `key`** - `_feeRate` is missing a check that it is no larger that 10000 (the dividing factor). - `_keyPrice` is missing a check that it is nonzero. - `_buyTime` is missing a check that it is at least the value of `block.timestamp`. --- **Contract `PreSalePool`** - `_salePhase` is missing a check that it is a strictly increasing value (old `_salePhase` inputs cannot be reused).
Addresses should be checked before assignment or external call to make sure they are not `address(0)`.
In contract `PreSalePool`, functions `buyTokenWithExactlyBUSD()` and `buyTokenWithExactlyBNB()` use function `verifyWhitelist()` to verify a `_candidate` address can participate in the presale and, for what amount of BUSD they are approved to purchase the presale token with. The input information in function `verifyWhitelist()` is then used in the `Whitelist` library function `verifySignature()`, along with the state variable `signer` address. This function uses `getMessageHash()` to hash together `_salePhase`, `_signer`, `_candidate`, and `_BUSDAmount`, which is then used to check against the provided `_signature` using OpenZeppelin's ECDSA `recover()` function. A `nonce` value is usually incorporated to prevent a user from using the same signature for the exact same message twice within the contract instance. Within the context of the `PreSalePool` contract, it is conceivable that this omission is by design, since the amount a `_candidate` address can purchase is checked against a running sum of what they have already purchased. If `maxBUSDUserCanSpend` for the `currentSalePhase` is a multiple of the `_BUSDAmount` a user is verified to use, then the user may call one of the two buying functions several times. However, if this is not the intention, then we recommend adding a `nonce` in the message hash to keep track of the exact message for which the signature was created. Moreover, specifications such as the name of the function, name of the project, version, and type of project are often used within the message hash to prevent the message from being used elsewhere. This information is typically encoded in a *domain separator* (note not all of this information necessarily must be included, if not relevant). Lastly, a chain ID is usually encoded as well in the event that a fork of the chain occurs. The concern is that a lack of this information within the message hash may allow for a signature to be reused, if the `signer` address is ever used in another context, and if the same generic information is ever used for a message hash. A similar set up is used within the cited functions in contract `dNFT`, creating the same potential issue.
The only time `lockToken()` is called is in function `temporaryMerge()`, where the owner of the token is checked prior, as is the status of whether the token is locked. These same two checks are made within the function `lockToken()` again and are unnecessary, since `lockToken()` is a private function. A similar issue occurs with `unlockToken()`.
A local variable is shadowing another component defined elsewhere. ```solidity=8 constructor(address _admin, uint256 _totalSupply) ERC20("GalactixCoin", "GXZ") { ``` - Local variable `_totalSupply` in `GalactixToken.constructor` shadows the variable `_totalSupply` in `ERC20`.
In contracts `dNFT` and `PreSalePool`, the amount of tokens a user is able to buy is restricted. In `dNFT`, it is restricted by value `_maxAmountUserCanBuy` for the sale phase. This value may be set as high as the `MAX_SALE_AMOUNT`. If the product of (`_maxAmountUserCanBuy`)*(number of users buying) exceeds the `MAX_SALE_AMOUNT`, then it is possible that some users may buy all tokens before others get the chance to purchase the tokens for which they are whitelisted to buy. In `PreSalePool`, a similar issue occurs where if the `maxBUSDUserCanSpend` allows a user to purchase an amount of tokens such that the product of (max amount user can buy)*(number of whitelisted users) exceeds `maxPreSaleAmount`, then some users may be able to purchase all available tokens for the sale phase before other whitelisted members have purchased any tokens at all.
Requiring that `BUSDToken` is transferred from the `from` address to the `treasuryAddress` as a tax may keep a third party who is supposed to be approved to transfer the NFT from being able to transfer the NFT. As constructed, the user who owns the NFT must both give approval to the third party user to move their tokens, but then must also approve the contract for the amount of tax on the `launchPrice` (in `key`, the `keyPrice` - neither are necessarily known ahead of time). Failure to do the second part will keep the third party from being able to transfer the NFT.
The linked `transfer()`/`transferFrom()` invocations do not check the return value of the function call, which should yield `true` in the case of a proper ERC-20 implementation.
Aggregators allow for the possibility of negative values as price. ```solidity uint256 BNBPerBUSDInUint256 = uint256(BNBPerBUSD); ``` The type conversion `uint256(BNBPerBUSD)` from type int256 to type uint256 may cause a negative value to be converted to a large, nonequivalent unsigned integer.
Using Solidity's `transfer()` and `send()` functions for transferring Ether is not recommended, since some contracts may not be able to receive the funds. These functions forward only a fixed amount of gas (2300 specifically) and the receiving contracts may run out of gas before finishing the transfer. Additionally, gas costs may increase in the future. Thus, some contracts that can receive now may stop working in the future due to the gas limitation.
Functions `buyTokenWithExactlyBUSD()` and `buyTokenWithExactlyBNB()` both use `verifyWhitelist()` as a check, where the former function uses input `_BUSDAmount` as `_BUSDAmount` for `verifyWhitelist()` and the latter uses `msg.value` as `_BUSDAmount` for `verifyWhitelist()`. Since verifying an amount of BUSD (for example, 100 BUSD) is different than verifying the same amount of BNB (e.g., 100 BNB), the `msg.value` and `_BUSDAmount` should not be interchangeable in the signature verification process. If a user inputs a signature intended to be used in `buyTokenWithExactlyBUSD()` into function `buyTokenWithExactlyBNB()`, they may be able to buy more tokens faster than other users. Since there is a limit on the total number of tokens that can be bought in a phase, this may give an unfair advantage that leaves other whitelisted users unable to buy tokens.
Any user with a valid `_signature` can buy tokens in any sale phase. Note that both functions `buyUsingBUSD()` and `buyUsingBNB()` both pass a check through `verifyBuySignature()` which uses the input `_salePhase` as part of the check. However, all `salePhaseStatistics` are for the `currentSalePhase` which does not have to be the input `_salePhase` value, since there is no check for this. Note that modifier `inSalePhase` only checks that the current `block.Timestamp` is within the range of the *current* sale phase, not the input `_salePhase`. This may allow more users than intended to purchase tokens in a given sale phase.
In functions set up for users to pay an amount of BNB, the only check is that the `msg.value` is not less than the `priceInBNB`. This means that a user may pay more than the `priceInBNB` and regardless, it will all go to the `treasuryAddress`.
Input values `_priceInBUSD`, `_priceAfter24Hours`, and `_maxAmountUserCanBuy` in function `deployNewSalePhase()` of `dNFT` are of type `uint` and as such are always nonnegative. The check that these values are less than 0 will always be false. The same is true of input `_BUSDAmount` in function `canBuyMoreToken()` of `PreSalePool`.
The report names a repository or files. Open the report to see its scope.
Repository and files the report names (7)
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