CertiK reviewed DeepLink on 16 Jan 2025: 25 findings, 0 critical; 20 marked fixed, 5 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 | 3 | 0 | 1 |
| Medium | 0 | 0 | 3 |
| Low | 2 | 0 | 16 |
In the `Token` contract, the `lockTransferAdmin` role can transfer tokens to a specified address and add a lock period. If the `isLockActive` is true, users must wait for the unlock period to end before they can transfer these tokens out. ```solidity function transfer(address to, uint256 amount) public virtual override returns (bool) { if (to == address(0) || amount == 0) { return super.transfer(to, amount); } if (isLockActive && walletLockTimestamp[msg.sender].length > 0) { require(canTransferAmount(msg.sender, amount), "Insufficient unlocked balance"); } return super.transfer(to, amount); } ``` However, users can authorize another address and use the `transferFrom()` function to transfer out the locked tokens.
These contracts do not protect the initializer. An attacker can front-run the `initialize` call and assume ownership of the logic contract. Once in control, the attacker can perform privileged operations, misleading users into believing that they are interacting with the legitimate owner of the upgradeable contract.
In the `MultiSigTimeLock` contract, the `Proposal` struct includes a variable `canExecuteAfterTimestamp` to record the time a proposal can be executed. Once this time is reached and the proposal has garnered enough approvals to meet the threshold, anyone can execute the proposal through the `executeProposal()` function. However, the contract does not set deadlines for different stages of the proposal, such as the voting period and execution period. This could lead to the following issues: 1. After the `canExecuteAfterTimestamp`, signers can still change their decisions, it may affect the execution schedule of proposals. 2. If a proposal with certain settings is not executed in time, a malicious user could execute the historical proposal after the new one, causing the new proposal to be overridden by the historical one. - A proposal sets Alice's minting quota to 10,000 ether and is approved but not executed in time. - A new proposal sets Alice's minting quota to 100 ether and is executed. - Alice can execute the historical proposal to revert the quota back to 10,000 ether. 3. The proposal only includes the `canExecuteAfterTimestamp`, which does not effectively reflect delayed execution. Signers could vote and execute right at the `canExecuteAfterTimestamp`.
Contract `Token` extends `ERC20PermitUpgradeable` contract, but the extended contract is not initialized by the current contract. Generally, the initializer function of a contract should always call all the initializer functions of the contracts that it extends. Also, it is recommended that `ERC20BurnableUpgradeable` be initiated as well since the dependency version may be varied.
The `MultiSigTimeLock` contract grants several critical privileges to its signers, which may also be assigned to the contract itself. These include: - `_authorizeUpgrade` - `addSigner` - `removeSigner` - `updateRequiredApproveCount` - `updateMinDelaySeconds` The concern is that if any single signer’s key is compromised, that individual could effectively destory the multi-signature and time-lock safeguards. For example, they might upgrade the contract or lower the approval threshold and delay parameters, effectively bypassing the intended security controls.
In the `MultiSigTimeLock` contract, within the `_createProposal` function, there are conflicting require statements between lines 115 and 118. These conflicting conditions may cause the function to revert unexpectedly when `block.timestamp` is equal to `_canVoteBeforeTimestamp`. ```solidity=115 require(block.timestamp <= _canVoteBeforeTimestamp, "Vote period passed"); ``` ```solidity=118 require(_canVoteBeforeTimestamp > block.timestamp, "Can vote before should be > current timestamp"); ```
In the `MultiSigTimeLock` contract, each stage of a proposal (voting, delay, and execution) is governed by the `minDelaySeconds` parameter. In the latest update, when a signer calls `approveProposal()` and the required approval threshold is met, the function updates `canExecuteAfterTimestamp`. We understand this is intended to allow earlier execution, for instance, in emergencies. However, we would like to confirm the following points with the team: 1. By updating `canExecuteAfterTimestamp` without adjusting the expiration, the execution period may extend beyond what was defined by the original `minDelaySeconds` intervals. 2. Accelerating the proposal could lead to out-of-order execution, and the current model **allows race condition** issues and the new design may increase the likelihood as well. 3. If the threshold is met and `canExecuteAfterTimestamp` is already updated, any subsequent approvals will continue to update `canExecuteAfterTimestamp`, potentially pushing execution time further out. This seems contradictory to the second point. We would appreciate clarification on whether these behaviors are intentional.
In the `MultiSigTimeLock` contract, the contract can set `requiredApproveCount` as the threshold for proposal approval during initialization, and signers can modify this threshold at any time. The verification range for `requiredApproveCount` is as follows: ```solidity require(newApprovedCount > 0, "Must have at least one"); require(newApprovedCount <= signers.length, "Too many required approve count"); ``` The lower limit of the range is inappropriate. According to the majority principle, the threshold should be greater than half of the number of signers. Additionally, when adding or removing signers, the threshold should also change with the number of signers.
The `requestSetMinter()` function returns the bytes of data for the `mint()` function, which may prevent the proposal from being executed. ```solidity function requestSetMinter(address minter, uint256 amount) external pure returns (bytes memory) { bytes memory data = abi.encodeWithSignature("mint(address,uint256)", minter, amount); return data; } ```
The `Token` contract inherits from the `OwnableUpgradeable` contract, but the contract does not have any functions exclusive to the `owner` role.
The `value` variable in the `Proposal` struct is used to record the native token sent to the target contract when executing the proposal. ```solidity function createProposal( address target, bytes memory data ) external onlySigner { uint256 _canExecuteAfterTimestamp = block.timestamp + minDelaySeconds; proposals[proposalCount] = Proposal({ target: target, data: data, value: 0, canExecuteAfterTimestamp: _canExecuteAfterTimestamp, executed: false }); emit ProposalCreated(proposalCount, target, 0, _canExecuteAfterTimestamp); approvals[proposalCount][msg.sender] = true; emit ProposalApproved(proposalCount, msg.sender); proposalCount++; } ``` However, this variable is always zero.
The smart contract fails to validate the uniqueness of signer addresses during the set process. This lack of validation could allow for duplicate addresses within the signer array, potentially undermining the integrity of the multi-sig approval mechanism.
There should always be events emitted in the sensitive functions that are controlled by centralization roles.
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. ```solidity=92 canUpgradeAddress = _canUpgradeAddress; ``` - `_canUpgradeAddress` is not zero-checked before being used. Similarily, the function `createUpgradeContractProposal` also not validate the given `_canUpgradeAddress` is zero-address or not
The above line lacks a non-zero address check for the input address. Including a zero address in `signers` may prevent the proposal votes from reaching the threshold. ```solidity function initialize( address[] memory _signers, uint256 _requiredApproveCount, uint256 _minDelaySeconds ) public initializer { require(_signers.length >= _requiredApproveCount, "Signers < required"); require(_requiredApproveCount > 0, "Required signatures must be > 0"); signers = _signers; requiredApproveCount = _requiredApproveCount; minDelaySeconds = _minDelaySeconds; } ``` If the `target` address of the proposal is a zero address, the proposal cannot be executed. ```solidity function createProposal( address target, bytes memory data ) external onlySigner { uint256 _canExecuteAfterTimestamp = block.timestamp + minDelaySeconds; proposals[proposalCount] = Proposal({ target: target, data: data, value: 0, canExecuteAfterTimestamp: _canExecuteAfterTimestamp, executed: false }); emit ProposalCreated(proposalCount, target, 0, _canExecuteAfterTimestamp); approvals[proposalCount][msg.sender] = true; emit ProposalApproved(proposalCount, msg.sender); proposalCount++; } ``` It is meaningless that setting a minting quota for the zero address. ```solidity function setMinter(address minter, uint256 amount) external onlyMultiSigTimeLockContract { require(amount <= maxSupply - alreadyMinted, "Max supply reached"); minter2MintAmount[minter] = amount; } ```
The `MultiLock` contract does not validate the given `_requiredApproveCount` and `_minDelaySeconds`, which are non-zero during the initialization. This omission leads to the following issues: 1. **Approval Threshold of 0:** The `requiredApproveCount` can be set to 0, allowing proposals to be executed immediately after they are proposed without any necessary approvals. 2. **Zero Waiting Time:** The `minDelay` can be set to 0, permitting proposals to be executed without any enforced waiting period. Additionally, the `updateMinDelaySeconds` function does not verify that the `newMinDelay` is greater than 0, potentially allowing the delay to be set to zero.
In the updated version, the proposer of a `Normal` proposal is automatically considered as having voted during the proposal creation. However, for other types of proposals, the proposer is required to cast an additional vote.
In the `setMinter` function, the minting amount for a minter is assigned as follows: ```solidity=77 minter2MintAmount[minter] = amount; ``` The code directly sets the `amount` in the `minter2MintAmount` mapping for the specified `minter`. However, if the minter already has an existing amount, this operation will overwrite the previous value.
The current implementation of the `if` clause is redundant and is not taking any effect. It can be simply removed, making the code clearer and more straightforward.
While revoking the approvals, the following validations will be performed to ensure the proposal is valid to be revoked: ```solidity require(block.timestamp <= proposal.canVoteBeforeTimestamp, "Vote period passed"); require(!proposal.executed, "Proposal already executed"); require(proposals[proposalId].target != address(0), "Invalid proposal"); require(approvals[proposalId][msg.sender], "Not approved yet"); ``` However, for the proposals related to the `MultiSigTimeLock` update, for example, `createAddSignerProposal`, the target address is always address(0). As a result, the corresponding revoking operation cannot be completed.
The report names a repository or files. Open the report to see its scope.
Repository and files the report names (2)
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