Taiko DAO Contracts - Taiko


Prepared by:

Halborn Logo

HALBORN

Last Updated 04/10/2025

Date of Engagement: February 19th, 2025 - February 24th, 2025

Summary

100% of all REPORTED Findings have been addressed

All findings

13

Critical

0

High

0

Medium

0

Low

1

Informational

12


1. Introduction

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.

2. Assessment Summary

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.


3. Test Approach and Methodology

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).


4. Static Analysis Report

4.1 Description

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.

4.2 Output

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.







5. RISK METHODOLOGY

Every vulnerability and issue observed by Halborn is ranked based on two sets of Metrics and a Severity Coefficient. This system is inspired by the industry standard Common Vulnerability Scoring System.
The two Metric sets are: Exploitability and Impact. Exploitability captures the ease and technical means by which vulnerabilities can be exploited and Impact describes the consequences of a successful exploit.
The Severity Coefficients is designed to further refine the accuracy of the ranking with two factors: Reversibility and Scope. These capture the impact of the vulnerability on the environment as well as the number of users and smart contracts affected.
The final score is a value between 0-10 rounded up to 1 decimal place and 10 corresponding to the highest security risk. This provides an objective and accurate rating of the severity of security vulnerabilities in smart contracts.
The system is designed to assist in identifying and prioritizing vulnerabilities based on their level of risk to address the most critical issues in a timely manner.

5.1 EXPLOITABILITY

Attack Origin (AO):
Captures whether the attack requires compromising a specific account.
Attack Cost (AC):
Captures the cost of exploiting the vulnerability incurred by the attacker relative to sending a single transaction on the relevant blockchain. Includes but is not limited to financial and computational cost.
Attack Complexity (AX):
Describes the conditions beyond the attacker’s control that must exist in order to exploit the vulnerability. Includes but is not limited to macro situation, available third-party liquidity and regulatory challenges.
Metrics:
EXPLOITABILITY METRIC (mem_e)METRIC VALUENUMERICAL 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
Exploitability EE is calculated using the following formula:

E=meE = \prod m_e

5.2 IMPACT

Confidentiality (C):
Measures the impact to the confidentiality of the information resources managed by the contract due to a successfully exploited vulnerability. Confidentiality refers to limiting access to authorized users only.
Integrity (I):
Measures the impact to integrity of a successfully exploited vulnerability. Integrity refers to the trustworthiness and veracity of data stored and/or processed on-chain. Integrity impact directly affecting Deposit or Yield records is excluded.
Availability (A):
Measures the impact to the availability of the impacted component resulting from a successfully exploited vulnerability. This metric refers to smart contract features and functionality, not state. Availability impact directly affecting Deposit or Yield is excluded.
Deposit (D):
Measures the impact to the deposits made to the contract by either users or owners.
Yield (Y):
Measures the impact to the yield generated by the contract for either users or owners.
Metrics:
IMPACT METRIC (mIm_I)METRIC VALUENUMERICAL 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
Impact II is calculated using the following formula:

I=max(mI)+mImax(mI)4I = max(m_I) + \frac{\sum{m_I} - max(m_I)}{4}

5.3 SEVERITY COEFFICIENT

Reversibility (R):
Describes the share of the exploited vulnerability effects that can be reversed. For upgradeable contracts, assume the contract private key is available.
Scope (S):
Captures whether a vulnerability in one vulnerable contract impacts resources in other contracts.
Metrics:
SEVERITY COEFFICIENT (CC)COEFFICIENT VALUENUMERICAL VALUE
Reversibility (rr)None (R:N)
Partial (R:P)
Full (R:F)
1
0.5
0.25
Scope (ss)Changed (S:C)
Unchanged (S:U)
1.25
1
Severity Coefficient CC is obtained by the following product:

C=rsC = rs

The Vulnerability Severity Score SS is obtained by:

S=min(10,EIC10)S = min(10, EIC * 10)

The score is rounded up to 1 decimal places.
SeverityScore Value Range
Critical9 - 10
High7 - 8.9
Medium4.5 - 6.9
Low2 - 4.4
Informational0 - 1.9

6. SCOPE

Files and Repository
(a) Repository: taiko-contracts
(b) Assessed Commit ID: eed36d3
(c) Items in scope:
  • src/adapted-dependencies/ITaikoL1.sol
  • src/conditions/StandardProposalCondition.sol
  • src/factory/TaikoDaoFactory.sol
↓ Expand ↓
Out-of-Scope: Third party dependencies and economic attacks.
Remediation Commit ID:
Out-of-Scope: New features/implementations after the remediation commit IDs.

7. Assessment Summary & Findings Overview

Critical

0

High

0

Medium

0

Low

1

Informational

12

Security analysisRisk levelRemediation Date
Inconsistent Snapshot for Encryption AgentsLowRisk Accepted - 03/13/2025
Unused componentsInformationalSolved - 03/13/2025
Public functions not invoked internallyInformationalSolved - 03/25/2025
Lack of account removal mechanism in registryInformationalSolved - 03/17/2025
Missing visibility modifierInformationalSolved - 03/14/2025
Missing input validationInformationalPartially Solved - 03/14/2025
Empty 'revert' statementInformationalSolved - 03/14/2025
Missing address validation in signer managementInformationalAcknowledged - 03/20/2025
Unhandled return valuesInformationalAcknowledged - 03/20/2025
Floating pragmaInformationalAcknowledged - 03/20/2025
Redundant use of `this` keywordInformationalSolved - 03/14/2025
Missing eventsInformationalAcknowledged - 03/20/2025
Typo in error nameInformationalSolved - 03/14/2025

8. Findings & Tech Details

8.1 Inconsistent Snapshot for Encryption Agents

//

Low

Description

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.solresolveEncryptionAccountAtBlock() 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
    }

BVSS
Recommendation

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.

Remediation Comment

RISK ACCEPTED: The Taiko Labs team has accepted the risk related to this finding.

References

8.2 Unused components

//

Informational

Description

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";

BVSS
Recommendation
  • 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.


Remediation Comment

SOLVED: The Taiko Labs team solved this finding in commit f80fb99 by following the mentioned recommendation.

Remediation Hash
References

8.3 Public functions not invoked internally

//

Informational

Description

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) {

BVSS
Recommendation

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.

Remediation Comment

SOLVED: The Taiko Labs team solved this finding in commit 939b67a by following the mentioned recommendation.


Remediation Hash
References

8.4 Lack of account removal mechanism in registry

//

Informational

Description

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.

BVSS
Recommendation

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.

Remediation Comment

SOLVED: The Taiko Labs team solved this finding in commit 2628cad by following the mentioned recommendation.

Remediation Hash
References

8.5 Missing visibility modifier

//

Informational

Description

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.

BVSS
Recommendation

Explicitly define the visibility of all variables in the contracts to enhance readability and reduce the potential for errors.

Remediation Comment

SOLVED: The Taiko Labs team solved this finding in commit 62b1785 by following the mentioned recommendation.

Remediation Hash
References

8.6 Missing input validation

//

Informational

Description

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)).


BVSS
Recommendation

Implement input validation to ensure that the input parameters are valid and within the expected ranges.

Remediation Comment

PARTIALLY SOLVED: The Taiko Labs team partially solved this finding in commit a8fa1e5 by verifying the _governanceERC20Base and _governanceWrappedERC20Base parameters against the zero address.

Remediation Hash
References

8.7 Empty 'revert' statement

//

Informational

Description

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.

BVSS
Recommendation

Consider creating a custom error for this specific condition, or reverting with a string (reason) for clarity.

Remediation Comment

SOLVED: The Taiko Labs team solved this finding in commit a8fa1e5 by following the mentioned recommendation.

Remediation Hash
References

8.8 Missing address validation in signer management

//

Informational

Description

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.

BVSS
Recommendation

Implement zero address validation to prevent adding invalid addresses as signers.

Remediation Comment

ACKNOWLEDGED: The Taiko Labs team made a business decision to acknowledge this finding and not alter the contracts.

References

8.9 Unhandled return values

//

Informational

Description

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(

BVSS
Recommendation

Ensure that the return values of external calls are handled appropriately throughout the contracts.

Remediation Comment

ACKNOWLEDGED: The Taiko Labs team made a business decision to acknowledge this finding and not alter the contracts.

References

8.10 Floating pragma

//

Informational

Description

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;

BVSS
Recommendation

Lock the pragma version and also consider known bugs (https://github.com/ethereum/solidity/releases) for the compiler version that is chosen.

Remediation Comment

ACKNOWLEDGED: The Taiko Labs team made a business decision to acknowledge this finding and not alter the contracts.

References

8.11 Redundant use of `this` keyword

//

Informational

Description

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.

BVSS
Recommendation

Call the isListedAtBlock() function directly without using the this keyword.

Remediation Comment

SOLVED: The Taiko Labs team solved this finding in commit eb18176 by following the mentioned recommendation.

Remediation Hash
References

8.12 Missing events

//

Informational

Description

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.

BVSS
Recommendation

Emit events for all state changes that occur as a result of administrative functions to facilitate off-chain monitoring of the system.

Remediation Comment

ACKNOWLEDGED: The Taiko Labs team made a business decision to acknowledge this finding and not alter the contracts.

References

8.13 Typo in error name

//

Informational

Description

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.

BVSS
Recommendation

Correct the typo in the error name to improve the readability of the codebase.

Remediation Comment

SOLVED: The Taiko Labs team solved this finding in commit eb18176 by following the mentioned recommendation.

Remediation Hash
References

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.

© Halborn 2025. All rights reserved.