Gaia Network
GaiaNet Token

Covered the GaiaNet token and time-locked governance contracts.
report id
0405
category
Token & RWA
language
Solidity
final report
February 5, 2025
lines of code
182
Content

Download audit report

1

Executive Summary

This document outlines the security review conducted by Nethermind Security for Gaia Network. GaiaNet is a decentralized platform that enables users to create, scale, and monetize AI agents tailored to their expertise. Each node includes a fine-tuned LLM, knowledge embedding, a vector database, API server, and plugin system, allowing deployment as virtual counterparts.

This review focuses on analyzing the contracts associated with the Gaia token and governance with time-lock. The team performed a review of 182 lines of code. Throughout the process, the GaiaNet team offered clarifications through several meetings and detailed documentation, including flow diagrams that illustrate the interactions between contracts. The audit was performed using: (a) manual analysis of the codebase, and (b) writing test cases.

Remarks about GaiaNet documentation

The test analysis for the reviewed contracts reveals gaps in coverage. The GaiaGovernor contract lacks tests capable of detecting business logic bugs, relying instead on setter-based tests. Similarly, the GaiaToken contract demonstrates low test coverage. Comprehensive test coverage is essential to ensure the reliability and security of smart contracts.

Audit assessment
Audit TypeSecurity Review
Initial ReportJan 24, 2025
Final ReportFeb 05, 2025
MethodsManual Review, Tests
Repositorygaia-smart-contracts
Commit Hash - Token and Governance88de70c18d
Final Commit Hashc4afda4d0b
DocumentationDocumentation, diagrams, and communication
Documentation AssessmentMedium
Test Suite AssessmentLow
Total findings
8
Fixed
4
Acknowledged
4
Mitigated
0
Unresolved
0

Findings by severity

  • Critical0
  • High0
  • Medium0
  • Low5
  • Informational3
  • Best Practices0

2

Audited Files

#ContractLoCCommentsRatioBlankTotal
1GaiaGovernor.sol12675.6%13146
2GaiaTimelock.sol1013130.0%225
3GaiaToken.sol4612.2%754
Total1822111.5%22225

4

System Overview

This section presents an overview of the GaiaToken, GaiaTimelock, and GaiaGovernor contracts.

4.1 GaiaToken - ERC20 Token for Governance

The GaiaToken contract is an ERC20 token that serves as the backbone of the Gaia ecosystem, providing both governance power and economic utility. Based on OpenZeppelin implementations, it includes additional functionalities such as token burning, voting, pausing, and permit-based approvals.

Core Functionalities:

  • Token Burning (ERC20Burnable): Holders can burn tokens, reducing the circulating supply.
  • Permit-Based Approvals (ERC20Permit): Allows users to approve transactions via signatures, reducing gas costs.
  • Governance and Voting (ERC20Votes): Tokens represent voting power, and users can delegate their votes without transferring tokens.
  • Ownership Control (Ownable): Provides an access control mechanism where an account can be granted exclusive access to particular functions.
  • Pausability (Pausable): Allows the contract owner to pause token transfers for instance.

Tokenomics and Allocation:

  • Total supply: 1,000,000,000 GAIA tokens.
  • Mining allocation: 40% of the total supply (400,000,000 GAIA) is reserved for mining and rewards.
  • Minting: All tokens are minted at deployment and assigned to the deployer.

Main public/external functions:

  • Token Transfer:
    • Users can transfer tokens via the standard ERC20 transfer() function.
    • With ERC20Permit tokens can be transferred in a single transaction via permit function.
    • Transfers automatically update voting power in governance via ERC20Votes.
  • Token Burning:
    • Token holders can burn their tokens using the burn() function from ERC20Burnable, reducing the circulating supply.
  • Delegation for Governance:
    • A user can delegate their voting power to another address (or themselves) using delegate().
    • Delegation does not transfer tokens, only voting rights.
  • Pausing Token Transfers (Emergency Feature):
    • The owner can pause transfers using the pause() function from Pausable.
    • When paused, no tokens can be transferred until unpaused.

4.2 GaiaTimelock - Secure Execution of Governance Proposals

The GaiaTimelock contract introduces a delay between a governance decision and its execution, ensuring security and transparency. It extends OpenZeppelin’s TimelockController, inheriting access control and execution mechanisms.

Core Functionalities:

  • Governance Execution Delay:
    • Approved proposals must wait for a minimum delay before execution, preventing instant changes and allowing stakeholders to review decisions.
  • Proposers & Executors:
    • Proposers: Addresses that can create proposals.
    • Executors: Addresses that execute approved proposals.
    • These roles are configurable through governance.
  • Admin Role:
    • The admin can manage proposers and executors to oversee governance execution.
  • Standardized Governance (TimelockController):
    • Ensures compatibility with other governance modules.

Constructor Parameters:

The code below presents the parameters received by the constructor:

constructor(
uint256 minDelay,
address[] memory proposers,
address[] memory executors,
address admin
) TimelockController(minDelay, proposers, executors, admin) {}

The values set during deployment are:

  • a) minDelay – Minimum delay before execution.
  • b) proposers – Addresses with the right to create proposals.
  • c) executors – Addresses authorized to execute approved proposals.
  • d) admin – The initial administrator.

4.3 GaiaGovernor - On-Chain Voting & Proposal Execution

The GaiaGovernor contract is the central governance mechanism of the Gaia network, enabling token holders to propose, vote on, and execute decisions. It extends OpenZeppelin’s Governor contracts, making governance transparent and decentralized.

Core Functionalities:

  • Proposal & Voting System (Governor):
    • Token holders can create proposals and vote.
    • A voting delay ensures time before voting starts.
    • If quorum and votes are met, the proposal is approved for execution.
  • Custom Governance Settings (GovernorSettings):
    • Defines the voting delay, the voting period, and proposal threshold.
  • Simple Vote Counting (GovernorCountingSimple):
    • Extension of Governor, considering three options for vote counting: Against, For, and Abstain.
  • Token-Based Voting (GovernorVotes):
    • Extracts voting weight from an ERC20Votes token.
  • Quorum Requirement (GovernorVotesQuorumFraction):
    • Requires a minimum percentage of votes for proposals to pass.
  • Timelock-Controlled Execution (GovernorTimelockControl):
    • Approved proposals are queued in the GaiaTimelock before execution.
    • Implements proposal queuing, execution, and cancellation mechanisms.

The code below presents the parameters received by the constructor:

constructor(
IVotes _token,
TimelockController _timelock,
uint48 _votingDelay,
uint32 _votingPeriod,
uint256 _proposalThreshold,
uint256 _quorumPercentage
)
Governor("GaiaGovernor")
GovernorSettings(_votingDelay, _votingPeriod, _proposalThreshold)
GovernorVotes(_token)
GovernorVotesQuorumFraction(_quorumPercentage)
GovernorTimelockControl(_timelock)
{}

The values set in these parameters represent:

  • a) _token – GaiaToken token used for voting.
  • _timelock – GaiaTimelock contract for managing execution delays.
  • b) _votingDelay – Delay before voting begins.
  • c) _votingPeriod – Duration of the voting period.
  • d) _proposalThreshold – Minimum tokens required to submit a proposal.
  • e) _quorumPercentage – Required participation rate for governance approval.

Overridden Functions: The contract overrides multiple functions to integrate different governance extensions:

  • Voting Mechanics:
    • votingDelay() – Returns voting delay time.
    • votingPeriod() – Returns the voting duration.
    • proposalThreshold() – Defines the minimum token balance to propose.
  • Quorum & Execution:
    • quorum(blockNumber) – Computes required votes for quorum.
    • state(proposalId) – Retrieves the state of a given proposal.
    • proposalNeedsQueuing(proposalId) – Determines if a proposal requires queuing.
  • Timelock Integration:
    • _queueOperations(...) – Queues a proposal for execution.
    • _executeOperations(...) – Executes a successful proposal.
    • _cancel(...) – Cancels a pending proposal.
    • _executor() – Returns the address authorized to execute proposals.

This ensures seamless governance, integrating proposal creation, voting, and timelock-controlled execution.

5

Risk Rating Methodology

The risk rating methodology used by Nethermind Security follows the principles established by the OWASP Foundation. The severity of each finding is determined by two factors: Likelihood and Impact.

Likelihood measures how likely the finding is to be uncovered and exploited by an attacker. This factor will be one of the following values:

  1. High: the issue is trivial to exploit and has no specific conditions that need to be met;
  2. Medium: the issue is moderately complex and may have some conditions that need to be met;
  3. Low: the issue is very complex and requires very specific conditions to be met.

When defining the likelihood of a finding, other factors are also considered. These can include but are not limited to motive, opportunity, exploit accessibility, ease of discovery, and ease of exploit.

Impact is a measure of the damage that may be caused if an attacker exploits the finding. This factor will be one of the following values:

  1. High: the issue can cause significant damage, such as loss of funds or the protocol entering an unrecoverable state;
  2. Medium: the issue can cause moderate damage, such as impacts that only affect a small group of users or only a particular part of the protocol;
  3. Low: the issue can cause little to no damage, such as bugs that are easily recoverable or cause unexpected interactions that cause minor inconveniences.

When defining the impact of a finding, other factors are also considered. These can include but are not limited to data and state integrity, loss of availability, financial loss, and reputation damage. After defining the likelihood and impact of an issue, the severity is determined according to the table below.

Impact / LikelihoodLowMediumHigh
HighMediumHighCritical
MediumLowMediumHigh
LowInfo / Best PracticesLowMedium
UndeterminedUndeterminedUndeterminedUndetermined

To address issues that do not fit a High, Medium or Low severity, Nethermind Security also uses three more finding severities: Informational, Best Practices, and Undetermined.

  1. Informational findings do not pose any risk to the application, but they carry some information that the audit team intends to pass to the client formally;
  2. Best Practice findings are used when some piece of code does not conform with smart contract development best practices;
  3. Undetermined findings are used when we cannot predict the impact or likelihood of the issue.

6

Issues

6.1 Cancellation in GaiaTimelock can't be called from the GaiaGovernor

FileFix commitTypeStatus
GaiaTimelock.sol—LowAcknowledged

The cancellation of the proposal is currently done in two places. The first cancellation is reachable by the public function GaiaGovernor.cancel(...) and can be called only before the voting period starts. The second proposal cancellation is reachable by the GaiaTimelock.cancel(...) and can be called when the voting period has ended, and the proposal is already in the queue. The GaiaGovernor.cancel(...) calls GovernorTimelockControl._cancel(...) which overrides Governor._cancel(...) and adds additional functionality:

function _cancel(
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
bytes32 descriptionHash
) internal virtual override returns (uint256) {
uint256 proposalId = super._cancel(targets, values, calldatas, descriptionHash);
// @audit: the additional logic that should allow for canceling the proposal after the voting has been done by calling the GaiaTimelock.cancel(....)
bytes32 timelockId = _timelockIds[proposalId];
if (timelockId != 0) {
// cancel
_timelock.cancel(timelockId);
// cleanup
delete _timelockIds[proposalId];
}
return proposalId;
}

This additional logic, however, is not reachable because it requires the proposal to be in the queue (after voting), but the public function from where it is called GaiaGovernor.cancel(...) requires the proposal to be before the voting.

function cancel(
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
bytes32 descriptionHash
) public virtual returns (uint256) {
uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash);

// @audit: the cancellation may only be called during the "Pending" state, which is before the voting period
_validateStateBitmap(proposalId, _encodeStateBitmap(ProposalState.Pending));
if (_msgSender() != proposalProposer(proposalId)) {
revert GovernorOnlyProposer(_msgSender());
}
return _cancel(targets, values, calldatas, descriptionHash);
}

To allow the cancellation of a proposal when it's already queued, the owner may grant the canceller role to another trusted address. This is, however not recommended by OpenZeppelin and stated in the comment in GaiaGovernorTimelock:

/**
* WARNING: Setting up the TimelockController to have additional proposers or cancellers besides the governor is very
* risky, as it grants them the ability to: 1) execute operations as the timelock, and thus possibly performing
* operations or accessing funds that are expected to only be accessible through a vote, and 2) block governance
* proposals that have been approved by the voters, effectively executing a Denial of Service attack.
*/

Recommendation

If the cancellation in the GaiaTimelock is not necessary, there is no need to take any action. However, if it should be reachable, consider adding another public function in GaiaGovernor that would allow calling cancellation on different states than "Pending". Alternatively, the trusted address may be granted the canceller role to call GaiaTimelock.cancel(...) directly in emergency situations. Note, however, that this solution brings DOS risk if such an address is compromised, as stated in the OZ comment above.

Resolution

Discovered a code segment that does not introduce security vulnerabilities, maintained for potential future architectural considerations.

6.2 Safeguarding Contract Operations by Disabling Ownership Renouncement

FileFix commitTypeStatus
GaiaToken.sol—LowFixed

The GaiaToken contract inherits from OpenZeppelin's Ownable contract, which provides functionality for the owner to renounce ownership. Once the ownership is renounced, the contract is left without an owner, any functionality restricted to the owner would become permanently inaccessible. For example, GaiaToken extends the Pausable contract, which provides pause and unpause functionalities that can only be executed by the owner.

// @audit this functionality should be disabled
function renounceOwnership() public virtual onlyOwner {
_transferOwnership(address(0));
}

Recommendation

To protect the token from accidental renouncement of ownership, it is advisable to explicitly prevent any attempts to renounce ownership. This can be achieved by overriding the renounceOwnership function in the GaiaToken contract. The implementation below ensures the continuity and integrity of pausable operations within the contract:

function renounceOwnership() public override onlyOwner {
revert("Ownership cannot be renounced");
}

Update from Nethermind Security

Commit c4afda4.

6.3 Signers cannot cancel permit signatures before the deadline in GaiaToken

FileFix commitTypeStatus
GaiaToken.sol—LowFixed

GaiaToken permit signature allows a signer to create an EIP-712 signature. However, once the signature is generated, the signer cannot cancel it before the deadline. This limitation arises because the contract is based on OpenZeppelin's ERC20Permit, which does not include an external function for manually increasing the nonce. Instead, it only provides the internal _useNonce function.

Recommendation

To enable signers to cancel their signatures, implement an external function that allows them to directly consume their nonce, effectively invalidating the signature. For example:

function consumeNonce() external {
_useNonce(msg.sender);
}

Update from Nethermind Security

Commit c4afda4.

6.4 GaiaToken cannot be paused

FileFix commitTypeStatus
GaiaToken.sol—LowFixed

The GaiaToken contract incorporates an emergency stop mechanism using OpenZeppelin's Pausable contract. The Pausable module provides internal functions to activate and deactivate the paused state, which should only be triggered by an authorized account. However, the GaiaToken contract does not expose external or public functions for utilizing the pausing mechanism.

Recommendation

Add external functions that invoke the internal functions to modify the contract's state. Since GaiaToken inherits from the Openzeppelin's Ownable contract, these functions should be restricted to execution exclusively by the contract owner.

Update from Nethermind Security

Commit c4afda4.

6.5 GaiaToken extends Pausable but does not add pausable functionality

FileFix commitTypeStatus
GaiaToken.sol—LowFixed

A pause feature enables the contract owner or authorized callers to temporarily suspend operations of the contract. The GaiaTokenEth contract extends the Pausable contract but does not implement the pause mechanism on any function, i.e., whenNotPaused and whenPause modifiers.

Recommendation

Add the operations that should be temporarily halted in the contract in case of an emergency.

Resolution

Fixed

Update from Nethermind Security

In the first round of reaudit, the issue was not fixed. We replied with the following consideration: “the issue is not fixed. To address the described problem, the functions that transfer, mint, or burn tokens should have a whenNotPaused modifier. Alternatively, this modifier can be added to the _udpate(...) function, which is a common function for all those“. In the second round of reaudit, the issues was fixed in the commit c4afda4.

6.6 Important deployment information

FileFix commitTypeStatus
GaiaGovernor.sol
GaiaTimelock.sol
—InfoAcknowledged

The deployment of the GaiaGovernor and GaiaTimelock is difficult since both contracts require each others' addresses. The recommended way of deployment is to first deploy GaiaTimelock and put empty arrays for executors and proposers in the constructor. Then after the deployment of the GaiaGovernor, admin should call the GaiaTimelock to grant the executor and proposer roles to the GaiaGovernor contract.

Recommendation

Consider following the proposed scenario during deployment. Additionally read the discussion of this topic here.

Resolution

The deployment of GaiaTimelock does not require the address of the GaiaGovernor contract.

Update from Nethermind Security

During the deployment of GaiaTimelock the proposers and executors are needed to grant proposer, canceller and executor roles (link to the OpenZeppelin implementation). The GaiaGovernor should hold those roles according to the current implementation, therefore it's address is needed during the GaiaTimelock construction.

6.7 Quorum value may be incorrect because of bridging

FileFix commitTypeStatus
GaiaGovernor.sol—InfoAcknowledged

To accept the proposal, a quorum of votes must be reached. The quorum is set by the protocol owner and is calculated as the sum of "for" and "abstain" votes proportional to all the voting power (total minted GAIA tokens). However, with the current setup, the 40% of all minted tokens are predicted to be bridged to the Base and used there for restaking. The bridged GAIA tokens would be frozen in the bridge contract but would still be accounted for in the calculation of the quorum. That leads to an incorrect quorum. Consider the following example: the owner mints 1000 GAIA tokens. The quorum is set to 10%, which is 100 tokens. 40% of those tokens are bridged to Base, leaving 600 tokens on Ethereum. To reach quorum during voting, the proposal must get at least 100 "for/abstain" votes, which constitutes 100/600, which makes a real quorum at 16.6% instead of the initial 10%. Note that the number of bridged tokens may change over time, changing the value of the real quorum.

Recommendation

This is a business decision from the client.

Resolution

No modifications for now. The quorum threshold can be manually lowered.

6.8 GaiaTimelock could allow anyone to execute proposals

FileFix commitTypeStatus
GaiaTimelock.sol—InfoAcknowledged

The GaiaTimelock contract extends OpenZeppelin's TimelockController contract, which enforces a delay between queueing and executing an operation. This contract introduces the flexibility to operate in two modes: only role and open role. The onlyRoleOrOpenRole modifier is used in the execute and executeBatch functions, as shown below:

function execute(
address target,
uint256 value,
bytes calldata payload,
bytes32 predecessor,
bytes32 salt
) public payable virtual onlyRoleOrOpenRole(EXECUTOR_ROLE) { ... }

function executeBatch(
address[] calldata targets,
uint256[] calldata values,
bytes[] calldata payloads,
bytes32 predecessor,
bytes32 salt
) public payable virtual onlyRoleOrOpenRole(EXECUTOR_ROLE) { ...}

Granting EXECUTOR_ROLE to the zero address ( address(0)) enables anyone to execute proposals. The GaiaTimelock contract should prohibit setting open roles unless explicitly allowed during deployment. Currently, as presented below, executors are set without validation, which could inadvertently permit open roles:

constructor(
uint256 minDelay,
address[] memory proposers,
address[] memory executors,
address admin
) TimelockController(minDelay, proposers, executors, admin) {}

Recommendation

You can follow the guidelines explained here. Make sure that this role is set before the deployer renounces its admin privileges in favor of GaiaTimelock contract itself (for details see TimelockController roles.

Resolution

We recognize that role validation is critical and guides future developers to implement proper access controls.

7

Documentation Evaluation

Software documentation refers to the written or visual information that describes the functionality, architecture, design, and implementation of software. It provides a comprehensive overview of the software system and helps users, developers, and stakeholders understand how the software works, how to use it, and how to maintain it. Software documentation can take different forms, such as user manuals, system manuals, technical specifications, requirements documents, design documents, and code comments. Software documentation is critical in software development, enabling effective communication between developers, testers, users, and other stakeholders. It helps to ensure that everyone involved in the development process has a shared understanding of the software system and its functionality. Moreover, software documentation can improve software maintenance by providing a clear and complete understanding of the software system, making it easier for developers to maintain, modify, and update the software over time. Smart contracts can use various types of software documentation. Some of the most common types include:

  • Technical whitepaper: A technical whitepaper is a comprehensive document describing the smart contract’s design and technical details. It includes information about the purpose of the contract, its architecture, its components, and how they interact with each other;
  • User manual: A user manual is a document that provides information about how to use the smart contract. It includes step-by-step instructions on how to perform various tasks and explains the different features and functionalities of the contract;
  • Code documentation: Code documentation is a document that provides details about the code of the smart contract. It includes information about the functions, variables, and classes used in the code, as well as explanations of how they work;
  • API documentation: API documentation is a document that provides information about the API (Application Programming Interface) of the smart contract. It includes details about the methods, parameters, and responses that can be used to interact with the contract;
  • Testing documentation: Testing documentation is a document that provides information about how the smart contract was tested. It includes details about the test cases that were used, the results of the tests, and any issues that were identified during testing;
  • Audit documentation: Audit documentation includes reports, notes, and other materials related to the security audit of the smart contract. This type of documentation is critical in ensuring that the smart contract is secure and free from vulnerabilities.

These types of documentation are essential for smart contract development and maintenance. They help ensure that the contract is properly designed, implemented, and tested, and they provide a reference for developers who need to modify or maintain the contract in the future.

8

Remarks & Test Suite

The GaiaNet team has provided clarifications through meetings and documentation that outline how the protocol is intended to function, including definitions for each type of node, reward computations, and flow diagrams illustrating interactions between contracts. However, the documentation lacks detailed information, which made it challenging to map some requirements to their implementation in the code. The reward calculations in the code differ from those outlined in the documentation entitled “Gaianet Token Economy Desgin”. This specification is essential to identify design flaws and validate the implementation of these requirements from a security perspective.

Test suite
71Tests passed0Failed

The complete test output is in the full report.

As auditors, we recommend that any changes or updates made to the audited codebase undergo a re-audit or security review to address potential vulnerabilities or risks introduced by the modifications. By conducting a re-audit or security review of the modified codebase, you can significantly enhance the overall security of your system and reduce the likelihood of exploitation. However, we do not possess the authority or right to impose obligations or restrictions on our clients regarding codebase updates, modifications, or subsequent audits. Accordingly, the decision to seek a re-audit or security review lies solely with you.

This report is based on the scope of materials and documentation provided by you to Nethermind in order that Nethermind could conduct the security review outlined in 1. Executive Summary and 2. Audited Files. The results set out in this report may not be complete nor inclusive of all vulnerabilities. Nethermind has provided the review and this report on an as-is, where-is, and as-available basis. You agree that your access and/or use, including but not limited to any associated services, products, protocols, platforms, content, and materials, will be at your sole risk. Blockchain technology remains under development and is subject to unknown risks and flaws. The review does not extend to the compiler layer, or any other areas beyond the programming language, or other programming aspects that could present security risks. This report does not indicate the endorsement of any particular project or team, nor guarantee its security. No third party should rely on this report in any way, including for the purpose of making any decisions to buy or sell a product, service or any other asset. To the fullest extent permitted by law, Nethermind disclaims any liability in connection with this report, its content, and any related services and products and your use thereof, including, without limitation, the implied warranties of merchantability, fitness for a particular purpose, and non-infringement. Nethermind does not warrant, endorse, guarantee, or assume responsibility for any product or service advertised or offered by a third party through the product, any open source or third-party software, code, libraries, materials, or information linked to, called by, referenced by or accessible through the report, its content, and the related services and products, any hyperlinked websites, any websites or mobile applications appearing on any advertising, and Nethermind will not be a party to or in any way be responsible for monitoring any transaction between you and any third-party providers of products or services. As with the purchase or use of a product or service through any medium or in any environment, you should use your best judgment and exercise caution where appropriate. FOR AVOIDANCE OF DOUBT, THE REPORT, ITS CONTENT, ACCESS, AND/OR USAGE THEREOF, INCLUDING ANY ASSOCIATED SERVICES OR MATERIALS, SHALL NOT BE CONSIDERED OR RELIED UPON AS ANY FORM OF FINANCIAL, INVESTMENT, TAX, LEGAL, REGULATORY, OR OTHER ADVICE.