Spirals Audit Report

Copyright © 2022 by Verilog Solutions. All rights reserved.

September 12, 2022

by Verilog Solutions

This report presents our engineering engagement with the Spirals dev team on their vault contracts on the Celo network. In this audit, we focus on the impact vault contracts and the migration functions as Spirals team plans to migrate from the old staking contracts to the new impact vault contract.

Project NameSpirals Protocol
Repository Linkhttps://github.com/spiralsprotocol/protocol
Commit HashFirst: ede0470; Final: ee1c18c;
Language Solidity
ChainCelo

About Verilog Solutions

Founded by a group of cryptography researchers and smart contract engineers in North America, Verilog Solutions elevates the security standards for Web3 ecosystems by being a full-stack Web3 security firm covering smart contract security, consensus security, and operational security for Web3 projects.

Verilog Solutions team works closely with major ecosystems and Web3 projects and applies a quality above quantity approach with a continuous security model. Verilog Solutions onboards the best and most innovative projects and provides the best-in-class advisory services on security needs, including on-chain and off-chain components.

Table of Contents

Service Scope

Service Stages

  1. Architecture Consultancy Service
    • Protocol Security & Design Discussion Meeting
      • As a part of the audit service, the Verilog Solutions team worked closely with the Spirals development team to discuss potential vulnerability and smart contract development best practices in a timely fashion. Verilog Solutions team is very appreciative of establishing an efficient and effective communication channel with the Spirals team, as new findings were exchanged promptly and fixes were deployed quickly, during the preliminary report stage.
  1. Smart Contract Auditing Service
    • The Verilog Solutions team analyzed the entire project using a detailed-oriented approach to capture the fundamental logic and suggested improvements to the existing code. Details can be found under Findings & Improvement Suggestions.

Methodology

  • Code Assessment
    • We evaluate the overall quality of the code and comments as well as the architecture of the repository.
    • We help the project dev team improve the overall quality of the repository by providing suggestions on refactorization to follow the best practice of Web3 software engineering.
  • Code Logic Analysis
    • We dive into the data structures and algorithms in the repository and provide suggestions to improve the data structures and algorithms for the lower time and space complexities.
    • We analyze the hierarchy among multiple modules and the relations among the source code files in the repository and provide suggestions to improve the code architecture with better readability, reusability, and extensibility.
  • Access Control Analysis
    • We perform a comprehensive assessment of the special roles of the project, including their authorities and privileges.
    • We provide suggestions regarding the best practice of privilege role management according to the standard operating procedures (SOP).
  • Off-Chain Components Analysis
    • We analyze the off-chain modules that are interacting with the on-chain functionalities and provide suggestions according to the SOP.
    • We conduct a comprehensive investigation for potential risks and hacks that may happen on the off-chain components and provide suggestions for patches.

Audit Scope

File
src/vaults/ImpactVault.sol
src/vaults/ImpactVaultManager.sol
src/vaults/SpiralsCeloImpactVault.sol
src/vaults/SpiralsCUSDImpactVault.sol
src/vaults/SPRL.sol
src/vaults/wmcUSD.sol
src/staking/SpiralsStaking.sol
src/staking/SpiralsStakingV2.sol
src/staking/migration/IMigrationToImpactVault.sol
src/staking/migration/SpiralsCUSDVaultMigration.sol
src/staking/migration/SpiralsStakingMigration.sol

Project Summary

Spirals Protocol is a lightweight protocol that redirects block rewards to climate impact. We work directly with validators to support their climate impact journey.

With Spirals, block rewards aren't used as profit. Instead, they are directed into a smart contract which is governed by all stakers and validators that contribute to the accumulation of block rewards. All stakeholders can collectively vote on which projects receive funding.

The Spirals ecosystem has 5 key stakeholders:

  • Celo holders can make a climate impact while earning yield
  • Validators can effortlessly make a climate impact and attract more delegators
  • Climate Experts are elected to a committee and have the authority to add and remove climate projects eligible for support by the Spirals Community
  • Natural Asset Tokens (which are approved by experts) are purchased proportional to member preferences
  • Members hold $SPRL and can vote on funding allocation & climate experts. You can become a member by earning $SPRL governance through staking, validating, or reviewing projects as a climate expert.

Spirals V1 System Architecture

There are two main components that shape the protocol, namely the ImpactVault contracts which define the assets and the functionality of the staking process of that asset, and the ImpactVaultManager.sol which manages the contracts that will be considered for calculating the yield that will be directed towards the climate impact. Spirals V1 consists mainly of three contracts:

ImpactVaultManager.sol

contains the logic for managing the ImpactVault contracts.

This contract mainly has the following functionality:

  • Register a new ImpactVault.
  • Remove a registered ImpactVault.
  • Harvest users’ yields and mint same amount of the SPRL governance tokens to users.

SpiralsCeloImpactVault.sol

contains the logic for staking CELO.

This contract mainly has the following functionality:

  • Set dependencies on the contract (registry and manager address).
  • Deposit CELO into the stCELO manager contract.
  • Withdraw from the stCELO manager contract.

SpiralsCUSDImpactVault.sol

contains the logic for staking cUSD.

This contract mainly has the following functionality:

  • Set dependencies on the contract (Moola contract address).
  • Deposit cUSD into the Moola lending pool and wrap mcUSD into wmcUSD.
  • Unwrap wmcUSD to mcUSC. Then withdraw cUSD from moola lending pool withmcUSD.

Use Case Scenario

Spirals is a staking platform that allows redirecting rewards towards positive climate impact. When you stake in this platform, Spirals stakes on your behalf on Celo’s core smart contracts. Instead of staking yield, stakers receive proof of climate impact as well as SPRL governance tokens to vote on how funding should be allocated.

Findings & Improvement Suggestions

SeverityTotalAcknowledgedResolved
High000
Medium222
Low222
Informational776

High

None ; )

Medium

  1. activeVotes ≥ value
    SeverityMedium
    Filesrc/staking/SpiralsStakingV2.sol#L93;
    Commitdc3d7e3;
    StatusResolved in commit ee1c18c;
    • Description

      The only requirements are activeVotes + pendingVotes >= _value, so there are following combinations:

      activeVotespendingVotes
      _value_value
      _value_value
      _value_value
      _value_value

      In the if-else condition, when activeVotes ≤ _value, pendingVotes can be bigger or smaller or equal to _value.

      if (activeVotes >= _value) {
            revokeActive(_value);
        } else {
            revokePending(_value);
        }
        
        unlock(_value);

      So in the else condition, the revoke pending will fail when pendingVotes < value.

    • Exploit Scenario
      1. Alice has less pendingVotes and activeVotes than the value that she wants to unstake, but the condition activeVotes + pendingVotes >= _value is still fulfilled.
      1. Alice calls the unstake() function since the property of the sum is fulfilled.
      1. The revokePending() function will fail.
    • Recommendations

      Add check to guarantee that pendingVotes >= value when activeVotes ≤ _value, i.e., contemplate this case in the else condition of the if statement.

  1. Yield cannot be locked in the SpiralsCUSDVaultMigration contract
    SeverityMedium
    Filesrc/staking/migration/SpiralsCUSDVaultMigration.sol;
    Commitd7debb4;
    StatusResolved in commit 6dcce64;
    • Description

      The yields cannot be withdrawn from the current CUSD vault and the migration only migrates the principals for users to the new impact vault. A function should be added to the contract to withdraw the yields from the current contract.

      The SpiralsCUSDVaultMigration.sol contract is a vault that supports yield-bearing tokens like mcUSD. User can only withdraw their principals back because of the fixed 1:1 asset and staked asset ratio. Their balance of vault token (SpiralsCUSDVaultMigration.balanceOf()) will always be the principal amount.

      When doing the migration for the CUSD vault, the SpiralsCUSDVaultMigration.transferAssetsToImpactVaultForAddress() will withdraw CUSD from the Moola lending pool and deposit the CUSD into the new impact vault for users. However, only principals are deposited into the new impact vault for users because the migration amount for the user is read from the function SpiralsCUSDVaultMigration.balanceOf() directly. So after migration, the yields will still stay in the current CUSD vault and no one can touch it. A function should be added to deal with those yields left in the contract after migration.

    • Exploit Scenario
      1. Owner executes migration.
      1. All users principles are withdrawn and deposited for them to the new impact vault.
      1. There rest of the staked asset in vault (yield) will still be locked in the vault.
    • Recommendations

      Calculate the respective yield for each of the users that have staked and send that yield to the ImpactVault contract.

    • Results

      Resolved in commit 6dcce64.

      Now the yield can be manual passed into the migration functions in both the SpiralsCUSDVaultMigration.sol and SpiralsStakingMigration.sol contracts to transfer it to the ImpactVault. As it is not easy to track the precise amount of yields each user earned, Spirals team decided to do an approximate calculation. The generated yields will be assigned to users proportionally to their share.

Low

  1. Unused events
    SeverityLow
    Filesrc/vaults/ImpactVaultManager.sol#L33, L34, L35; src/vaults/SpiralsCUSDImpactVault.sol#L24;
    Commitdc3d7e3;
    StatusResolved in commit ee1c18c;
    • Description

      Those specified events are defined but not used.

    • Exploit Scenario

      N/A.

    • Recommendations

      Use events when applicable. Operations that transfer value or perform critical operations should trigger events so that users and off-chain monitoring tools can account for important state changes.

  1. Incorrect event emission for critical operations
    SeverityLow
    Filesrc/vaults/ImpactVault.sol#L172;
    Commitdc3d7e3;
    StatusResolved in commit 90291c1;
    • Description

      Function withdrawYieldAsset() emits the same event as the withdraw() function. However, they perform different operations, namely withdraw() transfers the underlying staked asset to the receiver, and withdrawYieldAsset() transfers the yield asset. The WithdrawYield event is defined and should be used in the line specified.

    • Exploit Scenario

      N/A.

    • Recommendations

      Emit WithdrawYield event instead of Withdraw when a user withdraws yield.

    • Results

      Resolved in commit 90291c1.

      Event and function were renamed for more clarity.

Informational

  1. Floating solidity pragma version
    SeverityInformational
    FileGlobal;
    Commitdc3d7e3;
    StatusResolved in commit 90291c1;
    • Description

      Current smart contracts use ^0.8.10. And compilers within versions ≥ 0.8.10 and <0.9.0 can be used to compile those contracts. Therefore, the contract may be deployed with a newer or latest compiler version which generally has higher risks of undiscovered bugs.

      It is a good practice to fix the solidity pragma version if the contract is not designed as a package or library that will be used by other projects or developers.

    • Exploit Scenario

      N/A.

    • Recommendations

      Use the fixed solidity pragma version.

    • Results

      Resolved in commit 90291c1.

      The solidity pragma version is now fixed to 0.8.11.

  1. Unused Custom error
    SeverityInformational
    Filesrc/vaults/ImpactVault.sol#L35;
    Commitdc3d7e3;
    StatusResolved in commit 90291c1;
    • Description

      Custom error in line specified is defined but not used.

      error WithdrawExceedsYield(uint256 requested, uint256 total);
    • Exploit Scenario

      N/A.

    • Recommendations

      Remove the unused custom error or use it when the withdraw exceeds the yield.

    • Results

      Resolved in commit 90291c1.

      The unused custom error was removed.

  1. Typos
    SeverityInformational
    Filesrc/vaults/ImpactVault.sol#L136;
    Commitdc3d7e3;
    StatusResolved in commit 90291c1;
    • Description

      There’s a typo in line 136.

      * @notice Withdraws underlying asset by converting equivlaent value in
    • Exploit Scenario

      N/A.

    • Recommendations

      Correct the line specified to the following:

      * @notice Withdraws underlying asset by converting equivalent value in
    • Results

      Resolved in commit 90291c1.

      Typo was corrected.

  1. Missing fields in event
    SeverityInformational
    Filesrc/vaults/SpiralsCeloImpactVault.sol#L40;
    Commitdc3d7e3;
    StatusResolved in commit 90291c1;
    • Description

      Some important fields are missing in event DependenciesUpdated.

      Event DependenciesUpdated contains the information of two variables:

      event DependenciesUpdated(
            address indexed manager,
            address indexed registry
        );

      But function setDependencies() updates with the following three parameters:

      function setDependencies(
            address _stCeloTokenAddress,
            address _stCeloManagerAddress,
            address _celoRegistryAddress
        )

      The Celo registry address should also be included to the event field

    • Exploit Scenario

      N/A.

    • Recommendations

      Add missing field to the DependenciesUpdated event.

    • Results

      Resolved in commit 90291c1.

      The missing variable was added to the event.

  1. hasOutstandingWithdrawal() should also check if the value is non-zero
    SeverityInformational
    Filesrc/vaults/SpiralsCeloImpactVault.sol#L209;
    Commitdc3d7e3;
    StatusResolved in commit 7d48c3c;
    • Description

      Function _withdraw() calls hasOutstandingWithdrawal() to check if the current user has an outstanding withdrawal. Besides from timestamp, the value should also be checked otherwise user who input zero value to withdraw will trigger withdraw lock automatically.

      function hasOutstandingWithdrawal(address _address) public view returns (bool) {
            return withdrawals[_address].timestamp != 0;
        }

      When users try to withdraw funds from Celo impact vault, _withdraw() will called. It updates users withdraw timestamp with an unlocking period when user has outstanding withdraw and the withdraw value withe the value passed in by user.

      Whether the user qualifies the withdraw only depends on the timestamp in function hasOutstandingWithdrawal(). However, user might accidentally pass in a zero value and it will get user trapped in an unlocking waiting period and user can only withdraw zero value after that.

    • Exploit Scenario

      N/A.

    • Recommendations

      Also check if withdrawInfo.value is non-zero in hasOutstandingWithdrawal() function to prevent users accidentally lock themselves for an unlocking period of time.

    • Results

      Resolved in commit 7d48c3c.

      A non-zero value check is added to _beforeWithdraw().

  1. Redundant indexed in events
    SeverityInformational
    FileImpactVaultManager.sol#L33, L34; SpiralsCeloImpactVault.sol#L38, L39; SpiralsCUSDImpactVault#L24
    Commitdc3d7e3;
    StatusAcknowledged;
    • Description

      The parameter amount of the events is indexed. A numerical value usually should not be indexed as it won’t accelerate searching.

      event Receive(address indexed sender, uint256 indexed amount);
        event Claim(address indexed receiver, uint256 indexed amount);
    • Exploit Scenario

      N/A.

    • Recommendations

      Remove the indexed keyword from the numerical values in the events:

      event Receive(address indexed sender, uint256 amount);
        event Claim(address indexed receiver, uint256 amount);
    • Results

      Acknowledged.

  1. Deposit only when the amount is not zero
    SeverityInformational
    Filesrc/staking/migration/SpiralsStakingMigration.sol#L42; src/staking/migration/SpiralsCUSDVaultMigration.sol#L39;
    Commitd7debb4;
    StatusResolved in commit 6dcce64;
    • Description

      For function transferAssetsToImpactVaultForAddress(), a condition check can be added to make sure it only deposits into the new impact vault when the amount is not zero.

    • Exploit Scenario

      N/A.

    • Recommendations

      Add the following checks:

      function transferAssetsToImpactVaultForAddress(address _address, ImpactVault _vault) public onlyOwner whenPaused {
            /// ...
        
            if(amount != 0) {
                celoVault.depositCelo{value: amount}(_address);
            }
        
            /// ...
        }
      function transferAssetsToImpactVaultForAddress(address _address, ImpactVault _vault) public onlyOwner whenPaused {
            /// ...
        
            if(cUSDAmount != 0) {
                cUSDVault.deposit(cUSDAmount, _address);
            }
                
            /// ...
         }
    • Results

      Resolved in commit 6dcce64.

      Require statement was added to check if the deposit amount is zero.

Access Control Analysis

The workflow of the protocol consists of creating ImpactVault contracts that define the assets that can be staked and then registering them to the ImpactVaultManager.sol contract. This allows the ImpactVaultManager.sol contract to keep track of all the funds staked. The privileged roles and access control for this two-step process are the following:

ImpactVault contracts

  • The deployer of these contracts should define the principal asset, yield asset, and ImpactVaultManager address.
  • The ImpactVaultManager.sol contract can withdraw the yield accumulated by the asset.

ImpactVaultManager.sol

The deployer of the contract has control over the following functionality:

  • Set the governance token that will be associated with the yield generated by the ImpactValut contracts.
  • Register new ImpactVault contracts.
  • Remove a registered ImpactVault contract.

Appendix I: Severity Categories

SeverityDescription
HighIssues that are highly exploitable security vulnerabilities. It may cause direct loss of funds / permanent freezing of funds. All high severity issues should be resolved.
MediumIssues that are only exploitable under some conditions or with some privileged access to the system. Users’ yields/rewards/information is at risk. All medium severity issues should be resolved unless there is a clear reason not to.
LowIssues that are low risk. Not fixing those issues will not result in the failure of the system. A fix on low severity issues is recommended but subject to the clients’ decisions.
InformationalIssues that pose no risk to the system and are related to the security best practices. Not fixing those issues will not result in the failure of the system. A fix on informational issues or adoption of those security best practices-related suggestions is recommended but subject to clients’ decision.

Appendix II: Status Categories

StatusDescription
UnresolvedThe issue is not acknowledged and not resolved.
Partially ResolvedThe issue has been partially resolved.
AcknowledgedThe Finding / Suggestion is acknowledged but not fixed / not implemented.
ResolvedThe issue has been sufficiently resolved.

Disclaimer

Verilog Solutions receives compensation from one or more clients for performing the smart contract and auditing analysis contained in these reports. The report created is solely for Clients and published with their consent. As such, the scope of our audit is limited to a review of code, and only the code we note as being within the scope of our audit detailed in this report. It is important to note that the Solidity code itself presents unique and unquantifiable risks since the Solidity language itself remains under current development and is subject to unknown risks and flaws. Our sole goal is to help reduce the attack vectors and the high level of variance associated with utilizing new and consistently changing technologies. Thus, Verilog Solutions in no way claims any guarantee of security or functionality of the technology we agree to analyze.

InIn addition, Verilog Solutions reports do not provide any indication of the technologies proprietors, business, business model, or legal compliance. As such, reports do not provide investment advice and should not be used to make decisions about investment or involvement with any particular project. Verilog Solutions has the right to distribute the Report through other means, including via Verilog Solutions publications and other distributions. Verilog Solutions makes the reports available to parties other than the Clients (i.e., “third parties”) – on its website in hopes that it can help the blockchain ecosystem develop technical best practices in this rapidly evolving area of innovation.