CertiK reviewed MUMUBIT TOKEN (Metacraft) on 12 Mar 2025: 71 findings, 0 critical; 57 marked fixed, 14 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 | 2 | 0 | 2 |
| Medium | 0 | 0 | 11 |
| Low | 12 | 0 | 44 |
Within the contract, the `totalStakeAmount` variable signifies the current staked amount. However, in the `openStake()` function, there are no constraints on the parameters provided by the user, such as `_itemToken` and `_tokenAmount`. ```solidity function openStake( address _itemToken, uint256 _tokenAmount, uint256 _duration ) public whenNotPaused virtual { ... IERC20(_itemToken).transferFrom(msg.sender, address(this), _tokenAmount); ... totalStakeAmount+=_tokenAmount; stakeAmounts[_duration]+=_tokenAmount; ... } ``` If an attacker utilizes a malicious `itemToken` to execute this function, it could potentially manipulate `totalStakeAmount`, leading to a DoS attack. This situation also occurs in `stakeAmounts`.
In the `upgradeStake()` function, the contract calculates the user's reward using the remaining `tokenAmount` from `oldstake`, without updating the timestamp for reward reception afterward. This situation may allow the user to leverage the remaining `tokenAmount` from the current duration to repeatedly calculate and receive rewards. ```solidity function upgradeStake( address _itemToken, uint256 _tokenAmount, uint256 _oldDuration, uint256 _newDuration ) public whenNotPaused virtual { ... oldstake.tokenAmount -= _tokenAmount; ... payedProfits += stakeProfit; require(IERC20(_itemToken).approve(address(this), stakeProfit), "Approve has failed"); IERC20(_itemToken).transferFrom(address(this), oldstake.staker, stakeProfit); oldstake.reward = 0; // No update to oldstake.timestamp //totalStakeAmount -=_tokenAmount; if(newstake.staker == msg.sender){ ... }else { ... } ... } ``` Two potential attack scenarios arise: 1. Despite no restriction on upgrading to the same duration in `upgradeStake()`, an attacker could exploit this by upgrading to the same duration. In the `newstake`, the remaining tokenAmount from the current duration would be utilized to calculate rewards again. If the timestamp updates after calculating `oldstake`, upgrading to the same duration would not yield any rewards, as `newDuration_in_days` would be 0. 2. Following `upgradeStake()`, the attacker could utilize the `unStake()` function to withdraw the tokenAmount and its corresponding reward. As the duration's timestamp remains unchanged, the reward calculation within `unStake()` would still rely on the old timestamp. This situation may result in improper reward calculations and potentially exploit the system.
The array `validOwners` stores the current valid owner addresses. The `setOwner()` function either adds or removes valid owners based on the transaction operation. However, when adding a valid owner to the `validOwners` array, it does not check for duplicates, allowing re-addition of elements already present in the array. ```solidity function setOwner(address _owner, uint8 _status) public onlySelf { ... }else if(_status == 1) { validOwners.push(_owner); } } ``` Within the same function, removal of `validOwners` can be initiated based on the transaction. When scanning the `validOwners` array and encountering the target address, it will be removed. However, in the case of duplicate elements in the array, the `break` operation ensures that only the first matching address is removed, leaving subsequent duplicates unaffected in the array. This discrepancy between the length of `validOwners` and the count of valid signers will impact the execution of sign-related functions within the contract. ```solidity function setOwner(address _owner, uint8 _status) public onlySelf { _validOwners[_owner] = _status; if(_status == 0){ uint256 ownerLength = validOwners.length; for (uint256 i = 0; i < ownerLength; i++) { if (validOwners[i] == _owner) { validOwners[i] = validOwners[ownerLength - 1]; validOwners.pop(); break; } } } ... } ```
Usually, token URI points to data defining the NFT (attributes, images, etc.). It's usually set by the contract owner. A user that owns an NFT can just spoof any other NFT data by changing the token URI to any of the other NFTs. The `tokenURI()` function may return the same `tokenURI` for different tokenIds, leading to non-unique NFTs and making it difficult to distinguish between them. ```solidity function tokenURI(uint256 tokenId) public view virtual override(ERC721) returns (string memory) { ... // If both are set, concatenate the baseURI and tokenURI (via abi.encodePacked). if (bytes(_tokenURI).length > 0) { return string(abi.encodePacked(base, _tokenURI)); } ... } ```
The contract defines `MIN_SIGNATURES` as a constant value of 3. This setting adapts to changes for the number of valid owners (`ownerCount`) in the `signSetTransaction` function. However, there is no similar logic in the `executeSetTransaction` function. ```solidity // Function for authorized owners to sign pending transactions function signSetTransaction(uint transactionId) public validOwner notExecuted(transactionId) { ... uint signCount = ownerCount>=3?MIN_SIGNATURES:ownerCount; ... } ``` ```solidity function executeSetTransaction(uint transactionId) validOwner public { ... require(transaction.signatureCount >= MIN_SIGNATURES,"Signs Not Satisfied"); ... } ``` If the `removeOwner` function is used to reduce the number of owners to fewer than three, the system becomes inoperable for executing set transactions.
The contract defines `MIN_SIGNATURES` as a constant value of 3. This setting does not adapt to changes in the number of valid owners (`_validOwners`). ```solidity uint constant MIN_SIGNATURES = 3; ``` ```solidity function setOwner(address _owner, uint8 _status) public onlySelf { _validOwners[_owner] = _status; } ``` ```solidity if (_settransactions[transactionId].signatureCount == MIN_SIGNATURES) { if (transaction.txType == TransactionType.SetSalesAddress){ _settransactions[transactionId].readyForExecutionTimestamp = block.timestamp + observationPeriod; } else{ _settransactions[transactionId].readyForExecutionTimestamp = block.timestamp; executeSetTransaction(transactionId); } } ``` If the `removeOwner` function is used to reduce the number of owners to fewer than three, the system becomes inoperable for actions requiring multi-signature consensus, since it's impossible to gather the necessary number of signatures. The contract does not have a mechanism to adjust the `MIN_SIGNATURES` based on the current number of valid owners. This inflexibility could limit the contract’s operational capability following changes in ownership structure.
In an ERC20 contract, the `transferFrom()` or `safeTransferFrom()` function allows an authorized spender to transfer tokens on behalf of the owner. However, when the contract needs to transfer its own ERC20 tokens, it's recommended to use the `transfer()` or `safeTransfer()` function instead, because it is possible that the target token does not support transfer from the user itself. There are many similar operations in the contract. For example, in the `openStake()` function, this contract employs a combination of `approve` and `transferFrom` methods to transfer tokens to the target address. ```solidity function openStake( address _itemToken, uint256 _tokenAmount, uint256 _duration ) public whenNotPaused virtual { ... if(oldstake.staker == msg.sender){ ... require(IERC20(_itemToken).approve(address(this), stakeProfit), "Approve has failed"); IERC20(_itemToken).transferFrom(address(this), oldstake.staker, stakeProfit); } ... } ```
The contract employs `observationPeriod` and `maxPendingTime` to represent the time interval required for function execution, however, it directly uses the value 24, which translates to 24 seconds rather than 24 hours. ```solidity=120 uint256 public constant observationPeriod = 24 ; uint256 public constant maxPendingTime = 96 ; ``` ```solidity=335 // can not sign within once within 24 hours require(block.timestamp - transaction.timestamp >= 24,"Time Lockin for 24 Hours for each signers !"); ```
The specified function on the identified line is used to modify some critical or sensitive contract variables. However, it can be called by anyone as it has no access restriction. This enables malicious actors to call this and modify the `BaseURI` at will. ```solidity=398 function setBaseURI(string memory baseURI_) external { _baseURIextended = baseURI_; } ```
The `signSetParam` function lacks a necessary check to ensure that a transaction has been properly initialized through the `setParameters` function before it can be signed. This omission allows valid owners to increase the `signatureCount` on an uninitialized `TxSetParam` transaction object, which could still hold the default values of its parameters (`locked_` and `halted_`). ```solidity function signSetParam(uint transactionId) validOwner public { TxSetParam storage transaction = _txsetparams[transactionId]; // Transaction must exist // Cannot sign a transaction more than once require(signatures[transactionId][msg.sender] != 1, "Can't Sign Again with the same Account!"); signatures[transactionId][msg.sender] = 1; transaction.signatureCount++; transaction.timestamp= block.timestamp; emit TransactionSigned(msg.sender, transactionId); if (transaction.signatureCount >= MIN_SIGNATURES) { locked = transaction.locked_; halted = transaction.halted_; emit TransactionCompleted(msg.sender, block.timestamp, transactionId); deleteSetParamTx(transactionId); } } ``` As a result, it is feasible that valid owners can sign an uninitialized transaction, meet the minimum signature requirements, and execute changes to the contract's state without ever setting explicit parameters through `setParameters`.
To accommodate fluctuations in the number of `validOwners`, the contract utilizes `validOwners` to store current valid owner addresses and adjusts the required number of signatures for function execution accordingly. However, the potential addition of duplicate elements to the `validOwners` array, coupled with the failure to remove duplicates when deleting specified elements, may result in invalid owner addresses being retained within the array. ```solidity function signSetTransaction(uint transactionId) public validOwner notExecuted(transactionId) { ... uint ownerCount = validOwners.length; uint signCount = ownerCount>=3?MIN_SIGNATURES:ownerCount; if (_settransactions[transactionId].signatureCount == signCount) { ... } ... } ```
In the `upgradeStake()` function, users are allowed to upgrade their token staked from a lower to a higher duration level. The suggested upgradeable periods are 180, 356, and 730 days. Only when the duration is these three levels, users can get rewards by staking. ```solidity=353 uint256 newDuration_in_days = (block.timestamp.sub(newstake.timestamp)).div(SECONDS_IN_DAY); ///Seconds uint256 newstakeAPY = 0; uint256 newstakeProfit=0; if(newDuration_in_days == 180) newstakeAPY = newstake.tokenAmount.mul(half_APY).div(100).div(2); //half year if(newDuration_in_days == 365) newstakeAPY = newstake.tokenAmount.mul(full_APY).div(100); // full year if(newDuration_in_days == 730) newstakeAPY = newstake.tokenAmount.mul(two_APY).div(100).mul(2); // two years should multiply by 2 ``` When upgrading, if a user has previously staked tokens, the system will first calculate the existing staking rewards corresponding to the new duration. Therefore, it's important to assess the `newDuration` rather than the time interval `newDuration_in_days` since the user's initial staked.
The `addSetTransaction()` function implements a unified logic for the owner to add various set functions to pending transactions, such as `addOwner()`, `removeOwner()`, and `setParameters()` functions. However, its visibility is public, enabling the owner to bypass the set function and directly add pending transactions. This could lead to a lack of parameter checks or oversight of the length and status of current pending transactions.
As a best practice, the `nonReentrant` modifier could be placed and executed before other modifiers in functions to prevent reentrancy attacks through other modifiers and make the code more efficient.
In `upgradeStake()`, transitioning from a higher duration to a lower one is unrestricted. Consequently, if the user switches the current duration to 0, the duration check in unstake is effectively bypassed because it becomes 0. ```solidity function upgradeStake( address _itemToken, uint256 _tokenAmount, uint256 _oldDuration, uint256 _newDuration ) public whenNotPaused virtual { ... Stake storage oldstake = stakes[_itemToken][msg.sender][_oldDuration]; Stake storage newstake = stakes[_itemToken][msg.sender][_newDuration]; require(oldstake.staker == msg.sender, "staker address has no stake before in this duration"); ... if(newstake.staker == msg.sender){ ... }else { Stake memory stake = Stake({ itemToken: _itemToken, staker: msg.sender, tokenAmount: _tokenAmount, duration: _newDuration, reward : 0, timestamp:block.timestamp }); stakes[_itemToken][msg.sender][_newDuration] = stake; } ... } ``` In this scenario, user can promptly withdraw the tokenAmount that upgrades the duration without any waiting period.
We found that the newly declared `stakeTokenAddress` was initialized to a hard-coded [EOA address](https://etherscan.io/address/0x4fdB85CDa4eA74C5d55B45ACCc5dFaD58690A4F7) when the contract was deployed. We'd like to check with the team if this address is being used for testing purposes or if it will remain the same upon official deployment.
In the `setSalesAddress()` function, a new value is scheduled for update upon receipt of multiple validOwner signatures. However, during the signature waiting period, the old `feeAddress` remains active and is utilized in the `buyNode()` function to collect ERC20 Tokens from users. ```solidity function buyNode( address _to, string memory tokenURI_, address parentAddress //uint256 tokenPrice ) public returns (uint256){ ... ERC20Token.safeTransferFrom(msg.sender, feeAddress, mintPrice); ... } ```
The statements below could conflict if the `msg.sender` and the `receiver` are not the same address. This setup would make it logically impossible to pass both conditions unless `msg.sender` and `receiver` are identical. This might not be the intended logic, especially if the function is designed to allow a user (the current token owner) to specify another address (`receiver`) to receive the token. ```solidity function userFetchToken( uint256 tokenId, address receiver ) public nonReentrant notContract { require(tokenOwner[tokenId]== msg.sender,"Token Owner not Correct!"); require(tokenOwner[tokenId] == receiver, "Token receiver not Correct!"); ... } ```
The contract involves the transfer of ERC721 tokens, which means that the contract will be able to accept ERC721 tokens. However, the contract does not implement the `onERC721Received()` function. This can cause failures when others interact with it using `safeTransferFrom()`.
Using `transferFrom` for NFT transfers might result in the NFT being sent to a contract address incapable of handling it, thereby locking the NFT in that contract address. However, with `safeTransferFrom`, it verifies if `onERC721Received` is implemented in the receiver account, ensuring its capability to handle the NFT properly. ```solidity function userFetchToken( uint256 tokenId )public { ... ERC721(this).transferFrom(address(this), msg.sender, tokenId); ... } ```
The linked contract imports and uses OpenZeppelin's `Counters` contract. ```solidity=16 using Counters for Counters.Counter; ``` However, starting from version 0.5.0, Openzeppelin has deprecated the usage of the `Counters` contract: https://github.com/OpenZeppelin/openzeppelin-contracts/issues/4233
When the duration is set to 730, it implies that the stakeAPY should adhere to the two-year rule, that is, multiply by 2. However, potential precision loss during division calculations may lead to discrepancies from the expected outcome. For example: ```solidity=143 if(duration == 730) stakeAPY = stake.tokenAmount.mul(two_APY).div(100).mul(2); // two years should multiply by 2 ```
The `verified` function is somewhat redundant in the way it verifies the signature. The ECDSA library from OpenZeppelin provides a robust, tested method for recovering the signer address from a signature. Thus, it's unnecessary and less efficient to manually implement this functionality again using inline assembly. ```solidity require(recoveredSigner == expectedSigner, "Invalid signature"); ... require(ecrecover(ethSignedMessageHash, v, r, s) == expectedSigner, "Invalid signature malleability"); ```
The contract inherits from ERC721, which already includes a `transferFrom()` function that serves the same purpose as the `transNFT`. ```solidity function transNFT( address _from, address _to, uint256 tokenId ) public returns (uint256) { require(msg.sender == ownerOf(tokenId),"Only the owner of this Token could transfer It!"); transferFrom(_from,_to,tokenId); return tokenId; } ``` The `transferFrom` function already includes a check similar to `msg.sender == ownerOf(tokenId)`. ```solidity /** * @dev See {IERC721-transferFrom}. */ function transferFrom(address from, address to, uint256 tokenId) public virtual override { //solhint-disable-next-line max-line-length require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner or approved"); _transfer(from, to, tokenId); } /** * @dev Returns whether `spender` is allowed to manage `tokenId`. * * Requirements: * * - `tokenId` must exist. */ function _isApprovedOrOwner(address spender, uint256 tokenId) internal view virtual returns (bool) { address owner = ERC721.ownerOf(tokenId); return (spender == owner || isApprovedForAll(owner, spender) || getApproved(tokenId) == spender); } ``` Moreover, the `buyNode` function first mints the new `newItemId` from `address(0)` to `msg.sender` and then transfers this token from `msg.sender` to `address(this)`. This process can be optimized to one `_mint(address(this), newItemId)` or `_safeMint(address(this), newItemId)`.
In the `getStakeStatus()` function, users can provide a `tokenAddress` to query the `hasStake` status. However, at the end of the `openStake()` function, the `hasStake[msg.sender]` status, indicating whether the user has staked, is modified to `true`. ```solidity function getStakeStatus (address _tokenAddress) public virtual view returns (bool) { return hasStake[_tokenAddress]; } ``` ```solidity function openStake( address _itemToken, uint256 _tokenAmount, uint256 _duration ) public whenNotPaused virtual { ... hasStake[msg.sender] = true; ... } ```
In the `MCTPStakeNew` contract, the implementation of the `notContract()` check can not cover all scenarios. The check can be bypassed if the call is from the constructor of a smart contract or when the contract is destroyed. Because, in that case, the code size will also be zero. ```solidity modifier notContract(address _address) { uint256 codeSize; assembly { codeSize := extcodesize(_address) } require(codeSize == 0, "Contracts are not allowed"); _; } ``` The "isContract" function in the OpenZeppelin "Address" library uses the same implementation, but comments mention that "it's unsafe to rely on the check and it can be bypassed". Reference: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol
To mitigate centralization concerns, a multi-signature approach is adopted for adding or removing a `validOwner`. However, there's no time delay implemented for the signing or execution of transactions by the owner. Consequently, an `validOwner` could potentially exercise all their rights immediately after gathering three signatures. ```solidity function signUserAdd(uint transactionId) validOwner public { ... signatures[transactionId][msg.sender] = 1; ... emit TransactionSigned(msg.sender, transactionId); if (transaction.signatureCount >= MIN_SIGNATURES) { _owners[(transaction.owner)]=1; emit TransactionCompleted(transaction.owner, block.timestamp, transactionId); deleteAddUserTx(transactionId); } } ```
Addresses are not validated before assignment or external calls, potentially allowing the use of zero addresses and leading to unexpected behavior or vulnerabilities. For example, transferring tokens to a zero address can result in a permanent loss of those tokens. - `_withdrawAddress` is not zero-checked before being used.
1. Transactions initiated via the `setWithdrawAddress()`, `signUserAdd`, and `signUserDel` function enter a pending state. They will execute once endorsed by three `validOwners`. However, without any signatures or with insufficient approvals, these transactions linger indefinitely in the pending queue until they meet execution prerequisites. Consequently, an outdated transaction might later receive approval and unexpectedly alter its designation. 2. Once a transaction garners the requisite three `validOwner` signatures and surpasses the `observationPeriod`, it becomes executable at any moment. Executing a transaction at an unforeseen time could potentially disrupt contract operations. Moreover, the lack of sequencing for these transactions may lead to newer transaction outcomes being inadvertently overridden by older ones, causing irregular contract functionality.
In the `buyNode()` function, each `tokenId`'s `tokenURI` is set to the constant variable `nodeTokenURI`. As a result, the `tokenURI` for different `tokenIds` remains the same. There is no necessity to use mapping to store identical `tokenURI` content. ```solidity string public constant nodeTokenURI = "ipfs://QmZ1n7S2UHFmBbiGFzFHYsTJoLwbU29rTDquxPccSNqajg"; function buyNode( uint256 timestamp, bytes32 nonce, bytes32 message, bytes memory signature, address parentAddress, uint256 maxPrice ) nonReentrant notContract external returns (uint256){ ... _tokenURIs[newItemId] = nodeTokenURI; ... } ```
The deletion logic in the `deleteTransaction` function of the provided Solidity contract can be optimized. Currently, it uses a loop to shift all the elements after a certain index down by one to fill a gap created by deleting an element. This is not gas-efficient, especially if the array `_pendingTransactions` becomes large. ```solidity for(uint i = 0; i < _pendingTransactions.length; i++) { if (1 == replace) { _pendingTransactions[i-1] = _pendingTransactions[i]; } else if (transactionId == _pendingTransactions[i]) { replace = 1; } } delete _pendingTransactions[_pendingTransactions.length - 1]; _pendingTransactions.pop(); ``` Instead of shifting all elements after the deleted index, the last element of the array is moved into the spot of the element we want to remove. This reduces the cost of deleting an element from O(n) to O(1), since we only need to update one index position rather than potentially shifting a large number of elements.
The `MCTPStake` contract includes a mechanism intended to secure updating the withdrawal address, by requiring multiple signatures from designated `owners`. The function `setWithdrawAddress` initiates transaction proposals, while `signTransaction` is used by owners to approve these proposals. ```solidity function signTransaction(uint transactionId) validOwner public { ... require(block.timestamp - transaction.timestamp >= 24 hours,"Time Lockin 48 Hours for at Least 2 signers after Tx created! !"); ... if (transaction.signatureCount >= MIN_SIGNATURES) { withdrawAddress = payable(transaction.withdrawAddress_); emit TransactionCompleted(transaction.withdrawAddress_, block.timestamp, transactionId); deleteTransaction(transactionId); } } ``` The `signTransaction` function requires each signature to occur at least 24 hours apart, totaling a 48-hour period which is designed to be the time window for owners to sign. As soon as the minimum signature threshold is met, the proposed transaction is executed instantly. The timelock period serves as a voting/approval period for owners. However, there is no isolation of the approval period (for signing by the owners) and the execution period (the window during which users can observe the approved transaction before it takes effect). Specifically, once a transaction has obtained the minimum required signatures, it is immediately executed without an additional delay. This design overlooks the need for a separate period during which stakeholders can detect and respond to impactful administrative actions. Without this observation period, users might not be able to make informed decisions, such as withdrawing their stakes in response to changes they disagree with or find alarming.
For ERC-20 tokens, the number of decimals can vary. A common standard is 18 decimals, just like ETH, but some tokens might use fewer. For instance, USDC, a popular stablecoin, uses 6 decimals. ```solidity function openStake( address _itemToken, uint256 _tokenAmount, uint256 _duration ) public whenNotPaused virtual { require(_tokenAmount >= minStakeAmount,"stake tokenAmount too small!"); ... totalStakeAmount+=_tokenAmount; stakeAmounts[_duration]+=_tokenAmount; ... } ``` When staking, the function imposes a minimum stake amount with a decimal precision of 18. This raises the threshold and complicates the utilization of the function for tokens with decimals other than 18, such as USDC. Additionally, the `totalStakeAmount` and `stakeAmounts[_duration]` accumulate the `_tokenAmount` from different `_itemToken`, making the value of these two state variables meaningless since each `_itemToken` may have different decimals.
1. In `executeTransaction()` function. This function can be triggered by any account without owner privilege, as previously mentioned by the team. However, within this function, the usage of `deleteTransaction()` is confined to `validOwner`. Consequently, this constraint means that in practice, only a validOwner is capable of actually carrying out the `executeTransaction()` function, leading to a situation that diverges from the anticipated operational scenario. ```solidity function executeTransaction(uint transactionId) public { ... deleteTransaction(transactionId); } modifier validOwner() { require(_owners[msg.sender] == 1, "not Authorized Mulsig User !"); _; } function deleteTransaction(uint transactionId) validOwner private { ... } ``` 2. In `deleteMaxPendingTx()` function. This function is accessible through owner-controlled (`owner`) operations such as `addOwner()`, `removeOwner()`, and `setWithdrawAddress()`. Within its scope, it carries out the deletion of pending transactions that have exceeded their timeout limit. However, the transaction removal function is exclusively callable by a `validOwner`, implying that only individuals who possess both owner and validOwner statuses can effectively implement the `deleteMaxPendingTx()` function's intended actions. ```solidity function addOwner(address _owner) public { require(msg.sender==owner,"Only owner can set Parameters"); deleteMaxPendingTx(); ... } function deleteMaxPendingTx() private { ... deleteTransaction(txId); ... } modifier validOwner() { require(_owners[msg.sender] == 1, "not Authorized Mulsig User !"); _; } function deleteTransaction(uint transactionId) validOwner private { ... } ```
The logic to remove a transaction attempts to shift all the subsequent elements in `_pendingTransactions` array leftward after finding the specified transactionId. However, it lacks proper validation to determine if the `transactionId` actually exists in the array. If it's not found, the code sets replace to false and will eventually delete the last item of `_pendingTransactions` regardless of whether it relates to the specified `transactionId`. ```solidity function deleteTransaction(uint transactionId) validOwner public { uint8 replace = 0; for(uint i = 0; i < _pendingTransactions.length; i++) { if (1 == replace) { _pendingTransactions[i-1] = _pendingTransactions[i]; } else if (transactionId == _pendingTransactions[i]) { replace = 1; } } delete _pendingTransactions[_pendingTransactions.length - 1]; _pendingTransactions.pop(); delete _transactions[transactionId]; } ```
The `checkVerify` and `splitSignature` functions seem to be functions for testing purpose. According to the official Solidity [documentation](https://docs.soliditylang.org/en/v0.8.18/solidity-by-example.html#id3), the function `splitSignature()` does not use all security checks. A real implementation should use a more rigorously tested library, such as openzepplin’s version of this code.
A reentrancy attack can occur when the contract creates a function that makes an external call to another untrusted contract before resolving any effects. If the attacker can control the untrusted contract, they can make a recursive call back to the original function, repeating interactions that would have otherwise not run after the external call resolved the effects. *If the itemToken is a malicious contract, the attacker's profit will only affect the itemToken, making it an inefficient operation for them. However, the reentrancy vulnerability may still cause other issues in the middle of transaction.* Reentrancy may occur in the following contracts because of the malicious itemToken contract: - `openStake()`: Allow users to participate in staking. - `upgradeStake()`: Upgrade stake from lower duration to higher Duration Level.
The team uses a 3 of 3 multi-signature mechanism to weaken the centralization risk of the owner. However, changes in the `validOwner` might lead to operational uncertainties, especially in functions requiring multi-signature consensus. For instance, a transaction pending due to lack of enough approval from all valid owners might suddenly proceed if a new owner, who agrees, is introduced and signs off on it, thereby allowing the transaction to be executed and implemented.
The `signTransaction` function allows owners to sign a transaction for its execution. The condition for a transaction to become ready for execution (`transaction.readyForExecutionTimestamp`) is that it must be signed by at least `MIN_SIGNATURES` number of owners. Once this condition is met, the transaction is considered ready for execution after an observation period (`observationPeriod`, which is 24 hours in this case) has passed since the last signature was added. ```solidity function signTransaction(uint transactionId) validOwner public { ... if (transaction.signatureCount >= MIN_SIGNATURES) { transaction.readyForExecutionTimestamp = block.timestamp + observationPeriod; emit TransactionReadyForExecution(transaction.withdrawAddress_, transaction.readyForExecutionTimestamp, transactionId); } } ``` However, **if additional owners sign the transaction after it has already reached the minimum required signatures (`MIN_SIGNATURES`), the `transaction.readyForExecutionTimestamp` will be updated to 24 hours after the most recent signature**, pushing the execution window further into the future each time an additional signature is added. This behavior can potentially delay the execution of a transaction indefinitely, as long as additional owners continue to sign within each new 24-hour window.
In the `buyNode` function, the caller must provide a `message` parameter which is supposed to match a challenge generated internally within the function using `getChallenge`. This design pattern is typically used to verify that the caller is indeed who they claim to be by validating a signature against a known hash. However, if the `message` is simply a hash of known parameters that are already inputs to the function (`timestamp, nonce, parentAddress, msg.sender`), then passing `message` as an input parameter is redundant. ```solidity function buyNode( uint256 timestamp, bytes32 nonce, bytes32 message, bytes memory signature, address parentAddress, uint256 maxPrice ) nonReentrant notContract external returns (uint256){ require(!halted, "Node Halted!"); bytes32 challenge = getChallenge(timestamp, nonce, parentAddress, msg.sender); require(message == challenge,"message not correct!"); ... } ```
A state variable in a derived contract is shadowing a similarly named component in a parent contract. Employing variables with identical names, despite their distinct usage and access methods, can diminish the readability of the contract code. Variable `_owners` in `MetaCraftVNode` shadows the variable `_owners` in `ERC721`. In `MetaCraftVNode` contract: ```solidity=73 mapping(address => uint8) private _owners; ``` In `ERC721` contract: ```solidity mapping(uint256 => address) private _owners; ```
State variables that never change should be declared as `constant` to save gas. ```solidity=77 uint256 public observationPeriod = 24 hours; ``` - `observationPeriod` could be declared `constant`. ```solidity=78 uint256 public maxPendingTime = 96 hours; ``` - `maxPendingTime` could be declared `constant`.
At the beginning of executing the `buyNode()` function, the provided parameter `_to` is constrained to `msg.sender`. As there's no discrepancy between `_to` and `msg.sender`, this parameter appears redundant and can be removed from the function.
In the `unStake()` function, the parameter `withDuration` determines whether the user can unstake at any time. When `withDuration` is set to true, the unstake operation is restricted to occur outside the duration period. ```solidity=412 if(withDuration){ require( block.timestamp - stake.timestamp >= _duration * 1 days, //days for real project string(abi.encodePacked("UnStake should be More than ",_duration," Days")) ); } uint256 payprofit = 0; if(_duration == 180) payprofit = _tokenAmount.mul(half_APY).div(100).div(2); //half of year should divided by 2 if(_duration == 365) payprofit = _tokenAmount.mul(full_APY).div(100); // full year if(_duration == 730) payprofit = _tokenAmount.mul(two_APY).div(100).mul(2); // two years should multiply by 2 payedProfits += payprofit; uint256 payAmount = _tokenAmount.add(payprofit); require(IERC20(_itemToken).approve(address(this), payAmount), "Approve has failed"); IERC20(_itemToken).transferFrom(address(this), stake.staker, payAmount); ``` However, if the value is set to false, the unstake operation disregards the time difference when calculating the reward. Consequently, even if the user's stake duration is incomplete, they will still receive the same amount of rewards as if the stake had been completed for the full duration.
The `deleteTransaction()` function is designed to remove any `setWithdrawAddress` transactions issued by the contract, with the operation being restricted to addresses deemed as `validOwner`. However, due to the absence of further constraints, any compromise to the `validOwner` account may allow the hacker to take advantage of this authority, this could disrupt the regular operation of both the `signTransaction()` function and the `withdrawAddress` feature. **Scenario** For a transaction to proceed as planned, it necessitates the endorsement of at least two `validOwner` signatures. However, suppose `signer3` has signed a transaction using the `signTransaction()` function. But then the `signer3` is in an unsafe state. A situation could arise where after `signer1` has signed this transaction, `signer3` might immediately delete it using the `deleteTransaction()` function. This action renders `signer1's` signature ineffective, leading to the transaction's failure to execute.
The `buyNode` function first mints the token to the `msg.sender` and then immediately transfers it to `address(this)`. This involves two separate state changes on the blockchain which consume more gas than necessary. ```solidity _mint(msg.sender, newItemId); ... transferFrom(msg.sender, address(this), newItemId); ``` Instead, the function can mint the token directly to `address(this)` to save on gas costs associated with the token transfer transaction. This change would simplify the operation and reduce gas usage by eliminating the need for the `transferFrom` call.
The return values of the `transfer()` and `transferFrom()` calls in the smart contract are not checked. Some ERC-20 tokens' transfer functions return no values, while others return a bool value, they should be handled with care. If a function returns `false` instead of reverting upon failure, an unchecked failed transfer could be mistakenly considered successful in the contract.
Upon reviewing the `reStake()` function, it was observed that the contract calculates and transfers the user's rewards and then re-stakes the previously staked `tokenAmount` to the contract again, with the only alteration being the timestamp.
In the `upgradeStake()` function, users have the option to upgrade the token amount from the current duration to another duration. Currently, in the `upgradeStake()` function, the tokenAmount slated for upgrade is deducted from `oldstake` initially, and the remaining token reward is then calculated and disbursed to the user. This process results in the reward for the upgraded tokenAmount never being calculated and claimed. ```solidity=328 oldstake.tokenAmount -= _tokenAmount; ... uint256 stakeAPY = 0; uint256 stakeProfit=0; if(_oldDuration == 180) stakeAPY = oldstake.tokenAmount.mul(half_APY).div(100).div(2); //half year if(_oldDuration == 365) stakeAPY = oldstake.tokenAmount.mul(full_APY).div(100); // full year if(_oldDuration == 730) stakeAPY = oldstake.tokenAmount.mul(two_APY).div(100).mul(2); // two years should multiply by 2 ```
When a user intends to stake or deposit, the ERC20 token is transferred from the user's account to the receiving contract. If the `transferFrom()` function is utilized, the user must pre-authorize the spender. However, the MCTPStake contract calls an unnecessary `token.approve()` for itself, it doesn't impact subsequent token transfers from user to this contract. For instance, within the `openStake()` function, the contract will grant authorization to itself before transferring tokens. ```solidity function openStake( address _itemToken, uint256 _tokenAmount, uint256 _duration ) public whenNotPaused virtual { ... require(IERC20(_itemToken).approve(address(this), _tokenAmount), "Approval failed"); IERC20(_itemToken).transferFrom(msg.sender, address(this), _tokenAmount); ... } ```
In the `openStake()` function, a check ensures that the sum of `oldstake.tokenAmount` and `_tokenAmount` does not exceed `type(uint256).max`. ```solidity function openStake( address _itemToken, uint256 _tokenAmount, uint256 _duration ) public nonReentrant //anti re-entrancy whenNotPaused notContract(msg.sender) //anti contract address virtual { ... // stake more coins for a staking stage if(oldstake.staker == msg.sender){ ... require(oldstake.tokenAmount + _tokenAmount <= type(uint256).max, "uint256 type overFLow!"); ... oldstake.tokenAmount += _tokenAmount; } } ``` In Solidity, starting from version 0.8.0, arithmetic operations automatically check for overflow and underflow. This means that if an operation would result in a value outside the allowable range of the data type (for example, adding 1 to the maximum value of a `uint256`), the transaction will revert due to an overflow error. Given this, the explicit requirement `require(oldstake.tokenAmount + _tokenAmount <= type(uint256).max, "uint256 type overflow!");` in the `openStake` function is redundant. The Solidity compiler (version 0.8.0 and above) will automatically revert the transaction if `oldstake.tokenAmount + _tokenAmount` exceeds the maximum value.
The for loop contains repeated storage read access in the condition check. Given that the ending condition does not change in the for loop, the repeated storage read is unnecessary, and its associated high gas cost can be eliminated. ```solidity=156 for(uint i = 0; i < _pendingTransactions.length; i++) { ``` Loop condition `i < _pendingTransactions.length` accesses the `length` field of a storage array.
The `SafeMath` library is used unnecessarily. With Solidity compiler versions 0.8.0 or newer, arithmetic operations will automatically revert in case of integer overflow or underflow.
The three variables `locked_`, `nodeCount_`, and `mintPrice_` in the `Transaction` structure appear to be unused in the contract. ```solidity struct Transaction { address withdrawAddress_; bool locked_; uint256 nodeCount_; uint256 mintPrice_; uint8 signatureCount; uint256 readyForExecutionTimestamp; uint256 timestamp; } ```
Given that `mintPrice` is variable, users might experience inconsistencies between the expected and actual `mintPrice` when utilizing the `buyNode()` function. In this situation, the `address(this)` directly transfers users' ERC20 tokens, potentially causing losses because users sometimes approve the contract to use `type(uint256).max` ERC20 tokens. ```solidity function buyNode( address _to, string memory tokenURI_, address parentAddress //uint256 tokenPrice ) public returns (uint256){ ... ERC20Token.safeTransferFrom(msg.sender, feeAddress, mintPrice); ... } ```
The linked variable 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. ```solidity constructor( string memory _name, string memory _symbol, address _erc20Token, uint256 _mintPrice ) ERC721(_name, _symbol) { ... owner = msg.sender; ... } ```
In version 0.5.0, OpenZeppelin made structural changes to its code base. Specifically, the [ReentrancyGuard contract](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4551), initially located under the "security" path, has been relocated to the "utils" folder.
The report names a repository or files. Open the report to see its scope.
Repository and files the report names (21)
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