Prepared by:
HALBORN
Last Updated 04/10/2025
Date of Engagement: February 19th, 2025 - February 24th, 2025
100% of all REPORTED Findings have been addressed
All findings
13
Critical
0
High
0
Medium
0
Low
1
Informational
12
Taiko Labs
engaged Halborn
to conduct a security assessment on their smart contracts beginning on February 19th, 2025 and ending on February 25th, 2025. The security assessment was scoped to the smart contracts provided to Halborn. Commit hashes and further details can be found in the Scope section of this report.
The Taiko Labs
codebase in scope consists of a DAO protocol leveraged by Aragon.
Halborn
was provided 5 days for the engagement and assigned 2 full-time security engineers to review the security of the smart contracts in scope. The engineers are blockchain and smart contract security experts with advanced penetration testing and smart contract hacking skills, and deep knowledge of multiple blockchain protocols.
The purpose of the assessment is to:
Identify potential security issues within the smart contracts.
Ensure that smart contract functionality operates as intended.
In summary, Halborn
identified some improvements to reduce the likelihood and impact of risks, which were mostly addressed by the Taiko Labs
team. The main ones are the following::
Introduce require statements in the constructor or deployOnce to validate all parameters.
Provide an upgrade path for long-lived DAOs.
Capture the creator’s owner/agent and store the agent used for encryption.
Halborn
performed a combination of manual review of the code and automated security testing to balance efficiency, timeliness, practicality, and accuracy in regard to the scope of this assessment. While manual testing is recommended to uncover flaws in logic, process, and implementation; automated testing techniques help enhance coverage of smart contracts and can quickly identify items that do not follow security best practices.
The following phases and associated tools were used throughout the term of the assessment:
Research into architecture, purpose and use of the platform.
Smart contract manual code review and walkthrough to identify any logic issue.
Thorough assessment of safety and usage of critical Solidity variables and functions in scope that could led to arithmetic related vulnerabilities.
Local testing with custom scripts (Foundry
).
Fork testing against main networks (Foundry
).
Static analysis of security for scoped contract, and imported functions (Slither
).
Halborn
used automated testing techniques to enhance the coverage of certain areas of the smart contracts in scope. Among the tools used was Slither, a Solidity static analysis framework. After Halborn
verified the smart contracts in the repository and was able to compile them correctly into their abis and binary format, Slither was run against the contracts. This tool can statically verify mathematical relationships between Solidity variables to detect invalid or inconsistent usage of the contracts' APIs across the entire code-base.
The security team assessed all findings identified by the Slither software, however, findings with related to external dependencies are not included in the below results for the sake of report readability.
The findings obtained as a result of the Slither scan were reviewed, and many were not included in the report because they were determined as false positives.
EXPLOITABILITY METRIC () | METRIC VALUE | NUMERICAL VALUE |
---|---|---|
Attack Origin (AO) | Arbitrary (AO:A) Specific (AO:S) | 1 0.2 |
Attack Cost (AC) | Low (AC:L) Medium (AC:M) High (AC:H) | 1 0.67 0.33 |
Attack Complexity (AX) | Low (AX:L) Medium (AX:M) High (AX:H) | 1 0.67 0.33 |
IMPACT METRIC () | METRIC VALUE | NUMERICAL VALUE |
---|---|---|
Confidentiality (C) | None (I:N) Low (I:L) Medium (I:M) High (I:H) Critical (I:C) | 0 0.25 0.5 0.75 1 |
Integrity (I) | None (I:N) Low (I:L) Medium (I:M) High (I:H) Critical (I:C) | 0 0.25 0.5 0.75 1 |
Availability (A) | None (A:N) Low (A:L) Medium (A:M) High (A:H) Critical (A:C) | 0 0.25 0.5 0.75 1 |
Deposit (D) | None (D:N) Low (D:L) Medium (D:M) High (D:H) Critical (D:C) | 0 0.25 0.5 0.75 1 |
Yield (Y) | None (Y:N) Low (Y:L) Medium (Y:M) High (Y:H) Critical (Y:C) | 0 0.25 0.5 0.75 1 |
SEVERITY COEFFICIENT () | COEFFICIENT VALUE | NUMERICAL VALUE |
---|---|---|
Reversibility () | None (R:N) Partial (R:P) Full (R:F) | 1 0.5 0.25 |
Scope () | Changed (S:C) Unchanged (S:U) | 1.25 1 |
Severity | Score Value Range |
---|---|
Critical | 9 - 10 |
High | 7 - 8.9 |
Medium | 4.5 - 6.9 |
Low | 2 - 4.4 |
Informational | 0 - 1.9 |
Critical
0
High
0
Medium
0
Low
1
Informational
12
Security analysis | Risk level | Remediation Date |
---|---|---|
Inconsistent Snapshot for Encryption Agents | Low | Risk Accepted - 03/13/2025 |
Unused components | Informational | Solved - 03/13/2025 |
Public functions not invoked internally | Informational | Solved - 03/25/2025 |
Lack of account removal mechanism in registry | Informational | Solved - 03/17/2025 |
Missing visibility modifier | Informational | Solved - 03/14/2025 |
Missing input validation | Informational | Partially Solved - 03/14/2025 |
Empty 'revert' statement | Informational | Solved - 03/14/2025 |
Missing address validation in signer management | Informational | Acknowledged - 03/20/2025 |
Unhandled return values | Informational | Acknowledged - 03/20/2025 |
Floating pragma | Informational | Acknowledged - 03/20/2025 |
Redundant use of `this` keyword | Informational | Solved - 03/14/2025 |
Missing events | Informational | Acknowledged - 03/20/2025 |
Typo in error name | Informational | Solved - 03/14/2025 |
//
The system does not snapshot which encryption agent was tied to a signer at proposal creation, which can lead to a voting integrity issue if agents are changed during an active proposal. The SignerList.resolveEncryptionAccountAtBlock()
function uses the current EncryptionRegistry
mapping to resolve an approver’s owner/agent relationship, but only the signer’s listed status is checked at the historical block.
This means if an owner replaces their agent after the proposal was created, the new agent (who was not part of the original encrypted payload distribution) is now allowed to approve the pending proposal. Conversely, the original agent (who had the encrypted details) would no longer be recognized after removal (since appointerOf(oldAgent)
becomes 0 (zero) and thus cannot approve.
This dynamic can be problematic, as a newly appointed agent might cast a vote without actually knowing the proposal’s content (if the encryption key changed or wasn’t shared). It also lets a signer potentially rotate agents to influence a vote – for example, if an owner’s original agent was uncooperative, the owner could appoint a new agent who will blindly approve, and the contract would count it. While this doesn’t allow unauthorized entities to vote (the new agent is still appointed by a listed signer), it breaks the expectation that only those privy to the original proposal can vote on it. It slightly undermines the integrity of the emergency voting process by not locking in the “voting identity” (owner or their delegate) at proposal creation.
Code Location: SignerList.sol
– resolveEncryptionAccountAtBlock()
uses the current encryptionRegistry.appointerOf(_address)
without a historical reference. There is no storage of the agent at snapshot time in the Proposal
struct. The EmergencyMultisig _canApprove()
then bases approval on this potentially updated information.
function resolveEncryptionAccountAtBlock(address _address, uint256 _blockNumber)
public
view
returns (address _owner, address _agent)
{
if (isListedAtBlock(_address, _blockNumber)) {
// The owner + the agent
return (_address, settings.encryptionRegistry.getAppointedAgent(_address));
}
address _appointer = settings.encryptionRegistry.appointerOf(_address);
if (this.isListedAtBlock(_appointer, _blockNumber)) {
// The appointed agent votes
return (_appointer, _address);
}
// Not found, returning empty addresses
}
In the createProposal()
functions of the EmergencyMultisig and Multisig contracts, capture the creator’s owner/agent and store the agent used for encryption (since all approvers would likely use the same mapping at creation time). Then, in _canApprove()
, require that if an agent was set at creation for that owner, only that specific agent can approve.
Alternatively, treat an agent change as invalid for existing proposals: if appointerOf(msg.sender)
at the snapshot block doesn’t match the current appointer (meaning the agent changed), then reject the approval or require the original agent to vote. This ensures the individuals who actually have the decrypted proposal (the original agent or the owner with original key) are the ones voting.
Implementing a full snapshot of agent mappings might be complex; at minimum, document this behavior so the council knows not to change agents during active proposals. As a procedural mitigation, the Security Council should refrain from rotating encryption agents until all active emergency proposals are resolved.
RISK ACCEPTED: The Taiko Labs team has accepted the risk related to this finding.
//
Throughout the files in scope, there are several instances where components are declared but never used. Instances of this issue include:
In the StandardProposalCondition
contract, the dao
variable is declared but not used throughout the contract.
In the Multisig
contract the InvalidAddressListSource
error is declared but never used.
In the OptimisticTokenVotingPlugin
contract the ProposalCreationForbidden
error is declared but never used.
Additionally, it was identified that several imported contracts or interfaces are not used within the proposed scope.
- src/SignerList.sol
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
- src/factory/TaikoDaoFactory.sol
import {Addresslist} from "@aragon/osx/plugins/utils/Addresslist.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
- src/setup/EmergencyMultisigPluginSetup.sol
import {DAO} from "@aragon/osx/core/dao/DAO.sol";
- src/setup/MultisigPluginSetup.sol
import {DAO} from "@aragon/osx/core/dao/DAO.sol";
- src/setup/OptimisticTokenVotingPluginSetup.sol
import {ITaikoL1} from "../adapted-dependencies/ITaikoL1.sol";
Remove the unused dao
variable from the StandardProposalCondition
contract. Alternatively, if the variable is intended to be used, implement the necessary logic.
Remove unused custom errors and ensure that any relevant error checks consistently reference active custom errors. This maintains a clean codebase, reduces confusion, and makes the contract’s logic clearer for future readers and maintainers.
Remove unused imports in order to increase code maintainability and avoid unnecessary bloat.
SOLVED: The Taiko Labs team solved this finding in commit f80fb99
by following the mentioned recommendation.
//
The smart contracts in-scope include several functions that are declared as public
but are not invoked internally within the smart contracts. These functions are intended to be called only from external sources.
- src/DelegationWall.sol
function register(bytes memory _contentUrl) public {
function getCandidateAddresses() public view returns (address[] memory) {
function candidateCount() public view returns (uint256) {
- src/EmergencyMultisig.sol
function supportsInterface(bytes4 _interfaceId) public {
function getProposal(uint256 _proposalId) public {
function hasApproved(uint256 _proposalId, address _account) public view returns (bool) {
function execute(uint256 _proposalId, bytes memory _metadataUri, IDAO.Action[] calldata _actions) public {
- src/EncryptionRegistry.sol
function appointAgent(address _newAgent) public {
function setOwnPublicKey(bytes32 _publicKey) public {
function setPublicKey(address _accountOwner, bytes32 _publicKey) public {
function getRegisteredAccounts() public view returns (address[] memory) {
function getAppointedAgent(address _account) public view returns (address) {
- src/Multisig.sol
function getProposal(uint256 _proposalId) public {
function hasApproved(uint256 _proposalId, address _account) public view returns (bool) {
function execute(uint256 _proposalId) public {
- src/OptimisticTokenVotingPlugin.sol
function hasVetoed(uint256 _proposalId, address _voter) public view returns (bool) {
function getProposal(uint256 _proposalId) public {
function veto(uint256 _proposalId) public virtual {
function execute(uint256 _proposalId) public virtual {
function updateOptimisticGovernanceSettings(OptimisticGovernanceSettings calldata _governanceSettings) public {
function parseProposalId(uint256 _proposalId) public {
- src/SignerList.sol
function isListedOrAppointedByListed(address _address) public view returns (bool listedOrAppointedByListed) {
function getListedEncryptionOwnerAtBlock(address _address, uint256 _blockNumber) public {
function resolveEncryptionAccountAtBlock(address _address, uint256 _blockNumber) public {
function supportsInterface(bytes4 _interfaceId) public view virtual override returns (bool) {
- src/factory/TaikoDaoFactory.sol
function deployOnce() public {
function getSettings() public view returns (DeploymentSettings memory) {
function getDeployment() public view returns (Deployment memory) {
To improve code clarity and optimize gas usage, it is recommended to change the visibility of these functions from public
to external
.
The external
keyword is specifically designed for functions that are meant to be called from outside the contract, and it can result in more efficient code execution.
By making this change, you can enhance the readability and performance of the smart contracts.
SOLVED: The Taiko Labs team solved this finding in commit 939b67a
by following the mentioned recommendation.
//
The EncryptionRegistry
contract maintains an accountList
array that stores all registered accounts, but lacks functionality to remove accounts that are no longer active or have been removed as appointed agents. Accounts are added to this accountList
when they first appoint an agent or set a public key, but there is no corresponding mechanism to remove them when they become inactive or delisted.
As the accountList
grows indefinitely, operations that iterate through this list (like checking for existing accounts in appointAgent()
and _setPublicKey()
) will consume increasingly more gas. This creates a potential DoS vector where the list becomes so large that operations become prohibitively expensive or hit block gas limits.
Implement a removal mechanism that allows accounts to be removed from the accountList
when they are removed as appointed agents. This could be achieved through a cleanup during agent appointment or key setting operations.
SOLVED: The Taiko Labs
team solved this finding in commit 2628cad
by following the mentioned recommendation.
//
In the StandardProposalCondition
contract, the dao
and minDuration
variables are missing the visibility modifier.
By default, variables are set to internal
visibility. However, It is considered best practice to explicitly specify visibility to enhance clarity and prevent ambiguity. Clearly labeling the visibility of all variables and functions will help in maintaining clear and understandable code.
Explicitly define the visibility of all variables in the contracts to enhance readability and reduce the potential for errors.
SOLVED: The Taiko Labs team solved this finding in commit 62b1785
by following the mentioned recommendation.
//
Throughout the contracts in scope, there are several instances where input validation is missing. Instances if this issue include:
The _governanceERC20Base
and _governanceWrappedERC20Base
parameters are not checked against the zero address in the OptimisticTokenVotingPluginSetup
contract constructor.
The _minDuration
parameter is not validated to fall within a reasonable range in the StandardProposalCondition
contract constructor. A long duration could potentially lock the contract for an excessive period.
The _dao
parameter in the initialize()
function of the SignerList
contract is not validated against the zero address.
The _setPublicKey()
function in the EncryptionRegistry
contract does not validate the _publicKey
parameter against a valid length.
In the OptimisticTokenVotingPlugin contract, the votingToken
and taikoBridge
state variables are updated without checking whether the assigned address is the zero address (address(0)
).
Implement input validation to ensure that the input parameters are valid and within the expected ranges.
PARTIALLY SOLVED: The Taiko Labs team partially solved this finding in commit a8fa1e5
by verifying the _governanceERC20Base
and _governanceWrappedERC20Base
parameters against the zero address.
//
In the OptimisticTokenVotingPlugin contract, there is an empty revert
statement, as follows:
if (_taikoL1 == address(0)) revert();
In case the condition is met, the function call to the initialize()
function will revert without a descriptive error message.
Consider creating a custom error for this specific condition, or reverting with a string (reason) for clarity.
SOLVED: The Taiko Labs team solved this finding in commit a8fa1e5
by following the mentioned recommendation.
//
The SignerList
contract lacks validation against zero addresses when adding new signers through the initialize()
and addSigners()
functions. These functions rely on the internal _addAddresses()
function inherited from the Addresslist
contract, which does not perform zero address validation.
The addition of zero addresses could affect quorum calculations, by artificially inflating the number of available signers.
Implement zero address validation to prevent adding invalid addresses as signers.
ACKNOWLEDGED: The Taiko Labs team made a business decision to acknowledge this finding and not alter the contracts.
//
Several function calls throughout the contracts in scope ignore the return values of the functions they call. Ignoring return values can lead to unexpected behavior and may result in unanticipated outcomes.
Instances of unhandled return values include:
In src/EmergencyMultisig.sol:
proposal_.destinationPlugin.createProposal(
In src/Multisig.sol
proposal_.destinationPlugin.createProposal(
Ensure that the return values of external calls are handled appropriately throughout the contracts.
ACKNOWLEDGED: The Taiko Labs team made a business decision to acknowledge this finding and not alter the contracts.
//
Smart contracts should be deployed with the same compiler version and flags that they have been tested with thoroughly. Locking the pragma helps to ensure that contracts do not accidentally get deployed using, for example, an outdated compiler version that might introduce bugs that affect the contract system negatively.
During the analysis of the proposed scope, it was identified that all smart contracts are using a floating pragma, as follows:
pragma solidity ^0.8.17;
Lock the pragma version and also consider known bugs (https://github.com/ethereum/solidity/releases) for the compiler version that is chosen.
ACKNOWLEDGED: The Taiko Labs team made a business decision to acknowledge this finding and not alter the contracts.
//
In the resolveEncryptionAccountAtBlock()
function of the SignersList
contract, the this
keyword is used redundantly when calling the isListedAtBlock()
function.
if (this.isListedAtBlock(_appointer, _blockNumber)) {
// The appointed agent votes
return (_appointer, _address);
}
While using the this
keyword is not incorrect, it is redundant and may create gas overhead in this context.
Call the isListedAtBlock()
function directly without using the this
keyword.
SOLVED: The Taiko Labs team solved this finding in commit eb18176
by following the mentioned recommendation.
//
In the deployOnce()
function of the TaikoFactory
contract, state is modified. However, these changes are not reflected in any event emission. Additionally, in the execute()
function of the OptimisticTokenVotingPlugin
no events are emitted as well.
Emit events for all state changes that occur as a result of administrative functions to facilitate off-chain monitoring of the system.
ACKNOWLEDGED: The Taiko Labs team made a business decision to acknowledge this finding and not alter the contracts.
//
In the SignerList
contract there is a typo in an error name, where the word Registry
is misspelled as Regitry
.
error InvalidEncryptionRegitry(address givenAddress);
While this typo does not affect the functionality of the code, it can make the codebase harder to read and understand.
Correct the typo in the error name to improve the readability of the codebase.
SOLVED: The Taiko Labs team solved this finding in commit eb18176
by following the mentioned recommendation.
Halborn strongly recommends conducting a follow-up assessment of the project either within six months or immediately following any material changes to the codebase, whichever comes first. This approach is crucial for maintaining the project’s integrity and addressing potential vulnerabilities introduced by code modifications.
// Download the full report
Taiko DAO Contracts
* Use Google Chrome for best results
** Check "Background Graphics" in the print settings if needed