OpenZeppelin Blog

Auki Labs Audit

Written by OpenZeppelin Security | December 21, 2023

Table of Contents

Summary​

Type
DePIN
Timeline
From 2023-11-13
To 2023-11-15
Languages
Solidity​
Total Issues
13 (11 resolved, 1 partially resolved)
Critical Severity Issues
0 (0 resolved)
High Severity Issues
0 (0 resolved)
Medium Severity Issues
2 (2 resolved)
Low Severity Issues
2 (2 resolved)
Notes & Additional Information
9 (7 resolved, 1 partially resolved)

Scope

We audited the aukilabs/auki-contracts repository at commit 841aa7d.

In scope were the following files:

 src
├── AukiToken.sol
├── BurnContract.sol
├── RewardLiquidityPoolContract.sol
├── StakingContract.sol
└── UUPSProxy.sol

System Overview

Auki Labs is a company creating products that target the spatial computing space. Specifically, they provide solutions powered by their posemesh system, which has a decentralized infrastructure and leverages blockchain technology to incentivize both developers and operators. The AukiToken, an ERC-20-compliant token, is a utility token used for staking as well as burning. Developers are required to burn these tokens in order to generate posemesh credits. In addition, these tokens are required to be staked in order to maintain a reputation and can be slashed for malicious behavior. When tokens are burned, they are transferred into a liquidity pool contract where rewards for participating in the network are later distributed amongst the users.

Privileged Roles and Security Assumptions

The posemesh is currently a centralized system. While the team desires to gradually decentralize the project, for the earlier iterations, many of the components will be governed by Auki Labs (both on-chain and off-chain).

A co-managed MPC wallet that requires Auki MPC signatures along with the custodian signature (after fulfilling KYC requirements for the custodian) is responsible for deploying these contracts. As a result, this wallet is minted 1e28 Auki tokens when the AukiToken contract is deployed. Furthermore, it receives the DEFAULT_ADMIN_ROLE role for the AukiToken, BurnContract, RewardLiquidityPoolContract, and StakingContract contracts. Since it has the default admin role, this wallet can grant itself (or any other address) any of the roles in these contracts, as well as revoke the roles. As such, this wallet has the ability to upgrade the implementation for any of the upgradeable contracts in this repository.

Another MPC wallet that only requires Auki MPC signatures and approval without KYC is intended to have the PAUSER_ROLE, SLASHER_ROLE, REWARD_OPERATOR_ROLE, and CONFIG_ROLE roles. Although not specified explicitly in the code, the fact that this wallet will have the above roles was communicated to us through our messages with the Auki Labs team.

While this wallet cannot upgrade the code, nor grant or revoke roles, it is nonetheless an extremely privileged role. This MPC wallet can pause the AukiToken contract, which would prevent any mints, burns, or transfers, as well as any staking, slashing, or withdrawing. This wallet also can slash any stake at will, reward any address any amount up to the total amount of funds available, as well as the set the amount needed for a staker and the time before any staker can withdraw their funds.

The BurnContract is granted the MINTER_ROLE and BURNER_ROLE roles, and the StakingContract is granted the BURNER_ROLE role.

Medium Severity

Paused Token Contract Does Not Pause Allowance Changes

The AukiToken contract can be paused and unpaused by an address that has been granted the PAUSER_ROLE. Through the use of the whenNotPaused modifier on the _beforeTokenTransfer function, when the AukiToken contract is paused, no tokens can be transferred, minted, or burned. However, this does not prevent approvals, permits, and/or any other allowance changes from taking place while the contract is paused.

According to the Auki Labs team, "the expected behavior when [Auki] token is paused is to pause all operations.". As such, consider adding the whenNotPaused modifier to all corresponding allowance change functions to prevent this from occurring while the contract is paused.

Update: Resolved in pull request #48 at commit 5c27964.

Unsafe Use of DEFAULT_ADMIN_ROLE

The AukiToken, BurnContract, RewardLiquidityPoolContract, and StakingContract contracts have access control (or its upgradeable counterpart). The DEFAULT_ADMIN_ROLE role is given to the msg.sender when deploying the contract, which is the co-managed MPC wallet. The admin role for all other roles in this system is the DEFAULT_ADMIN_ROLE role, since it is not explicitly assigned in the code. Since this role has special privileges associated with it, for example the ability to grant and revoke roles, further security measure should be taken.

According to the comments in OpenZeppelin's AccessControl.sol file as well as the official documentation, the recommendation is to use AccessControl in conjunction with AccessControlDefaultAdminRules. This contract implements the following risk mitigations on top of AccessControl:

  • Only one account holds the DEFAULT_ADMIN_ROLE role since deployment until it is potentially renounced.
  • Enforces a two-step process to transfer the DEFAULT_ADMIN_ROLE role to another account.
  • Enforces a configurable delay between the two steps, with the ability to cancel before the transfer is accepted. The delay can be changed by scheduling.
  • It is not possible to use another role to manage the DEFAULT_ADMIN_ROLE role.

Consider adding AccessControlDefaultAdminRules on top of the existing AccessControl in order to safely manage the DEFAULT_ADMIN_ROLE role.

Update: Resolved in pull request #48 at commit ce493df.

Low Severity

Remove Test Code isProxied From Constructors

There are instances of test code across several upgradeable contract constructors:

The isProxied parameter is used by the development team in order to use the Blacksmith tool. Blacksmith currently does not support interacting with a contract that uses a proxy upgrade pattern. However, deploying these contracts without the use of a proxy in production can create the risk of an attacker front-running the initialize function. This would lead to a redeployment of the contracts and wastage of gas.

Consider removing the isProxied parameter from the constructors in a production-ready codebase.

Update: Resolved in pull request #48 at commit 3597610.

Missing Event Emissions

Throughout the codebase, the constructors and initializers do not emit events after initializing sensitive variables in the system. Although in some cases, when those variables are updated using setter functions, an event is emitted:

  • In the initialize function of BurnContract, both storage variables token and recipient are set without emitting any events.
  • In the constructor of RewardLiquidityPoolContract, the storage variable token is set without emitting an event.
  • In the initialize function of StakingContract, the storage variable stakingToken is set without emitting an event. Furthermore, the storage variables thawingPeriod and stakeAmount are set without emitting the UpdatedThawingPeriod and UpdatedStakeAmount events, respectively.

Consider emitting events when updating state variables to more easily enable off-chain monitoring for the protocol.

Update: Resolved in pull request #48 at commit edd27f0.

Notes & Additional Information

Multiple Instances of Missing Named Parameters in Mappings

Since Solidity 0.8.18, developers can utilize named parameters in mappings. This means mappings can take the form of mapping(KeyType KeyName? => ValueType ValueName?). This updated syntax provides a more transparent representation of a mapping's purpose.

Throughout the codebase, there are multiple mappings without named parameters:

Consider adding named parameters to the mappings in order to improve the readability and maintainability of the codebase.

Update: Resolved in pull request #48 at commit 25f7718.

Variable Cast is Unnecessary

In StakingContract, there are multiple variables that are unnecessarily casted:

To improve the overall clarity, intent, and readability of the codebase, consider removing these unnecessary casts.

Update: Resolved in pull request #48 at commit 437191d.

Use of Foundry-Specific Imports

Throughout the codebase, the current format for importing other Solidity files is from the lib folder of the repository. For example, the OpenZeppelin contract imports take the form of import {SomeContract} from "lib/openzeppelin-contracts/*". While this is suitable for developers working only in Foundry, other developers who are using this contract (particularly those who are not using Foundry as their smart contract development framework) may run into compilation errors when trying to integrate.

For ease of consumption, particularly for other development frameworks such as Hardhat and Remix, consider changing the import format to import {SomeContract} from "@openzeppelin/contracts/..." as described in the OpenZeppelin documentation. This pattern can similarly be applied for other imports outside the OpenZeppelin library. In conjunction with this, a remappings.txt file can be added in the home directory to map imports to their Foundry specific paths.

Consider changing the import format as suggested above and adding a remappings.txt. This will help improve the developer experience for developers working in different development frameworks.

Update: Resolved in pull request #48 at commit 496066f.

Gas Optimizations

Potential gas cost improvements were found throughout the codebase:

  • Extraneous require statement on token transfers. The AukiToken contract inherits from the OpenZeppelin libraries which already revert with an error message if a transfer fails.
  • for loops that are using i++ can be optimized by upgrading to Solidity version 0.8.22 where the compiler will automatically generate an unchecked block to save gas.
  • Variables of the Stake struct are being individually updated across several functions in StakingContract.sol. The struct can be saved into a local storage variable that can then be used and updated as needed. This applies to the following functions:
  • A temporary local variable can be used to store the array length used in the for loops and require statements in RewardLiquidityPoolContract.sol. Specifically, the recipients.length in the following functions could be stored in a temporary variable:
  • The calldata keyword can be used instead of memory for dynamic array function parameters when the function is being called externally and the parameter is not manipulated within the function itself. This leaves the data inside of the calldata, rather than copying it to memory, which saves gas. The following examples were found in the codebase:

Consider applying these gas optimizations.

Update: Partially resolved in pull request #48 at commit 265a519. The local variables were added, the calldata keyword was used in the appropriate locations, and all the extraneous require statements were removed. The for loops were left unoptimized. The Auki Labs team stated:

We are constrained to use 0.8.19.

Naming Suggestion

In RewardLiquidityPoolContract, the balance function does not refer to the token balance of the contract, but rather is the token balance with the totalPendingReward subtracted from it. Consider updating the name of this function to nonPendingBalance for clarity and to prevent name-clashing with the commonly used name balance.

Consider applying the above naming suggestion to improve the consistency and readability of the codebase.

Update: Resolved in pull request #48 at commit e42401c.

RewardLiquidityPoolContract Is Not Upgradeable

Currently, RewardLiquidityPoolContract is a non-upgradeable contract. Therefore, in the event that there is a need to update its code, the storage variables will not persist once a new version is deployed (i.e., the _rewards mapping will be reset). To preserve the values of the storage variables even after an upgrade, consider making RewardLiquidityPoolContract upgradeable.

Update: Resolved in pull request #48 at commit ce493df.

Superfluous virtual Keyword

In StakingContract, the _stake and _withdraw functions have the virtual keyword. This indicates to a reader that these functions are intended to be overridden. However, the Auki Labs team has stated that they "do not plan to inherit from these contracts".

Consider removing the virtual keyword to show clear intent to other developers and end users who are reading the code.

Update: Resolved in pull request #48 at commit 768c7eb.

Lack of immutable Keyword for Unchanging Variables

In BurnContract, RewardLiquidityPoolContract, and StakingContract, the variable representing the AukiToken is a state variable. However, it cannot be changed by any function in the contract.

Consider adding the immutable keyword to this variable along with moving the variable initialization into the constructor. This will help save gas as well as improve code readability.

Update: Resolved in pull request #48 at commit d4c88ad.

.env Does Not Follow Best Practice

The .env file that has been pushed to Github contains a PRIVATE_KEY used for Anvil testing. Although there are no real funds on this network, it is best to have sensitive information stored offline and only an example stored in an .env.example file. Doing this will help prevent accidentally pushing a mainnet private key to Github, potentially resulting in the theft of funds.

Consider removing sensitive information from the .env file on Github and only leaving an example in an .env.example file.

Update: Acknowledged, not resolved. The Auki Labs team stated:

The leaked private key is the one provided by the Anvil tool. It is already a leaked key and is used by everyone using Anvil from the Foundry toolchain. As such, we may keep it to ease the developer workflow.

 

Conclusion

Auki Labs aim to utilize blockchain technology to incentivize developers and operators through the AukiToken. The token is burned for posemesh credits, staked for reputation, and can be slashed for malicious behavior, with the rewards being distributed from a liquidity pool contract.

This was the first audit that the OpenZeppelin team has performed for Auki Labs. In the report, we uncovered a couple of medium-severity issues related to the integration of the OpenZeppelin libraries. We would like to thank the Auki Labs team for their quick and thorough responses which greatly helped our process.

The codebase was well-tested with nearly 100% coverage. While we understand that this project is still in its infancy and therefore in a centralized state, we hope that over time, it will slowly decentralize into a protocol with less trust assumptions and privileged roles.