Download audit report
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 Type | Security Review |
| Initial Report | Jan 24, 2025 |
| Final Report | Feb 05, 2025 |
| Methods | Manual Review, Tests |
| Repository | gaia-smart-contracts |
| Commit Hash - Token and Governance | 88de70c18d |
| Final Commit Hash | c4afda4d0b |
| Documentation | Documentation, diagrams, and communication |
| Documentation Assessment | Medium |
| Test Suite Assessment | Low |
Findings by severity
| # | Contract | LoC | Comments | Ratio | Blank | Total |
| 1 | GaiaGovernor.sol | 126 | 7 | 5.6% | 13 | 146 |
| 2 | GaiaTimelock.sol | 10 | 13 | 130.0% | 2 | 25 |
| 3 | GaiaToken.sol | 46 | 1 | 2.2% | 7 | 54 |
| Total | 182 | 21 | 11.5% | 22 | 225 |
| # | Finding | Severity | Update |
| 1 | Cancellation in GaiaTimelock can't be called from the GaiaGovernor | Low | Acknowledged |
| 2 | Safeguarding Contract Operations by Disabling Ownership Renouncement | Low | Fixed |
| 3 | Signers cannot cancel permit signatures before the deadline in GaiaToken | Low | Fixed |
| 4 | GaiaToken cannot be paused | Low | Fixed |
| 5 | GaiaToken extends Pausable but does not add pausable functionality | Low | Fixed |
| 6 | Important deployment information | Info | Acknowledged |
| 7 | Quorum value may be incorrect because of bridging | Info | Acknowledged |
| 8 | GaiaTimelock could allow anyone to execute proposals | Info | Acknowledged |
This section presents an overview of the GaiaToken, GaiaTimelock, and GaiaGovernor contracts.
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:
ERC20Burnable): Holders can burn tokens, reducing the circulating supply.ERC20Permit): Allows users to approve transactions via signatures, reducing gas costs.ERC20Votes): Tokens represent voting power, and users can delegate their votes without transferring tokens.Ownable): Provides an access control mechanism where an account can be granted exclusive access to particular functions.Pausable): Allows the contract owner to pause token transfers for instance.Tokenomics and Allocation:
Main public/external functions:
transfer() function.ERC20Permit tokens can be transferred in a single transaction via permit function.ERC20Votes.burn() function from ERC20Burnable, reducing the circulating supply.delegate().pause() function from Pausable.unpaused.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:
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:
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:
Governor):GovernorSettings):GovernorCountingSimple):GovernorVotes):ERC20Votes token.GovernorVotesQuorumFraction):GovernorTimelockControl):GaiaTimelock before execution.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:
GaiaToken token used for voting.GaiaTimelock contract for managing execution delays.Overridden Functions: The contract overrides multiple functions to integrate different governance extensions:
votingDelay() – Returns voting delay time.votingPeriod() – Returns the voting duration.proposalThreshold() – Defines the minimum token balance to propose.quorum(blockNumber) – Computes required votes for quorum.state(proposalId) – Retrieves the state of a given proposal.proposalNeedsQueuing(proposalId) – Determines if a proposal requires queuing._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.
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:
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:
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 / Likelihood | Low | Medium | High |
| High | Medium | High | Critical |
| Medium | Low | Medium | High |
| Low | Info / Best Practices | Low | Medium |
| Undetermined | Undetermined | Undetermined | Undetermined |
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.
| File | Fix commit | Type | Status |
GaiaTimelock.sol | — | Low | Acknowledged |
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.
| File | Fix commit | Type | Status |
GaiaToken.sol | — | Low | Fixed |
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.
GaiaToken| File | Fix commit | Type | Status |
GaiaToken.sol | — | Low | Fixed |
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.
GaiaToken cannot be paused| File | Fix commit | Type | Status |
GaiaToken.sol | — | Low | Fixed |
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.
GaiaToken extends Pausable but does not add pausable functionality| File | Fix commit | Type | Status |
GaiaToken.sol | — | Low | Fixed |
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.
| File | Fix commit | Type | Status |
GaiaGovernor.solGaiaTimelock.sol | — | Info | Acknowledged |
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.
| File | Fix commit | Type | Status |
GaiaGovernor.sol | — | Info | Acknowledged |
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.
GaiaTimelock could allow anyone to execute proposals| File | Fix commit | Type | Status |
GaiaTimelock.sol | — | Info | Acknowledged |
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.
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:
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.
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.
| 71Tests passed | 0Failed |
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.
The Leading Engineers of Blockchain Infrastructure