Halborn Logo

KlimaDAO Autocompounder - KlimaDAO


Prepared by:

Halborn Logo

HALBORN

Last Updated 12/17/2024

Date of Engagement by: November 22nd, 2024 - November 26th, 2024

Summary

100% of all REPORTED Findings have been addressed

All findings

19

Critical

0

High

0

Medium

0

Low

7

Informational

12


1. Introduction

KlimaDAO engaged Halborn to conduct a security assessment on their smart contracts beginning on November 22th, 2024 and ending on November 26th, 2024. 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 KlimaDAO codebase in scope mainly consists of a set of smart contracts designed to interact with the Aerodrome Protocol in the Base network.

2. Assessment Summary

Halborn was provided 3 days for the engagement and assigned 1 full-time security engineer to review the security of the smart contracts in scope. The engineer is a blockchain and smart contract security expert 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 acknowledged by the KlimaDAO team:

    • Dynamically calculate the amountOutMin value, to set the desired slippage tolerance that will not affect drastically the swap execution.

    • Implement a more robust deadline parameter in order to restrict the time frame during which a trade can be executed.

    • Consider transitioning control to a multi-signature wallet setup for critical functions, establishing community-driven governance for decision-making on fund management, and/or integrating time locks.

    • Consider using OpenZeppelin's Ownable2StepUpgradeable contract over the OwnableUpgradeable implementation.

    • Perform multiplication before division to avoid precision loss.

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

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

4.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}

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

5. SCOPE

Files and Repository
(a) Repository: autocompounder
(b) Assessed Commit ID: a9ff58e
(c) Items in scope:
  • contracts/BIFI/strategies/Common/GreenFeeManagerInitializable.sol
  • contracts/BIFI/strategies/Aerodrome/StrategyAerodromeGaugeGreen.sol
  • contracts/BIFI/vaults/AutoRetireGreenVault.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.

6. Assessment Summary & Findings Overview

Critical

0

High

0

Medium

0

Low

7

Informational

12

Security analysisRisk levelRemediation Date
No slippage protection on UniswapV2 interactionsLowRisk Accepted - 12/10/2024
Unrestricted deadlines for swapsLowRisk Accepted - 12/10/2024
Centralization risksLowRisk Accepted - 12/10/2024
Single step ownership transfer processLowRisk Accepted - 12/10/2024
Precision loss due to division before multiplicationLowSolved - 12/10/2024
Potential reentrancy behavior when harvestingLowRisk Accepted - 12/10/2024
Inconsistent use of SafeERC20 library functionsLowRisk Accepted - 12/10/2024
Missing input validationInformationalAcknowledged - 12/10/2024
Use of outdated librariesInformationalAcknowledged - 12/10/2024
Missing check for possible division by zeroInformationalAcknowledged - 12/10/2024
Possible unauthorized movement of fundsInformationalPartially Solved - 12/10/2024
Inconsistency in harvest mechanismInformationalAcknowledged - 12/10/2024
Ignored return valuesInformationalAcknowledged - 12/10/2024
Use of magic numbersInformationalAcknowledged - 12/10/2024
Unused componentsInformationalSolved - 12/10/2024
Public functions can be marked externalInformationalSolved - 12/10/2024
Floating pragmaInformationalAcknowledged - 12/10/2024
Use of revert strings over custom errorsInformationalAcknowledged - 12/10/2024
Incomplete NATSPEC documentationInformationalAcknowledged - 12/10/2024

7. Findings & Tech Details

7.1 No slippage protection on UniswapV2 interactions

// Low

Description

In the addLiquidity() and the _chargeGreenFee() functions of the StrategyAerodromeGaugeGreenFee contract, there are several interactions with the Aerodrome router that do not include slippage protection. In current interactions with Aerodrome Protocol the amountOutMin argument which is used for slippage tolerance is set to 0 or 1. Setting the amountOutMin to 0 tells the swap that the caller will accept a minimum amount of 0 output tokens from the swap, which essentially means 100% slippage tolerance, opening up the caller to a risk of loss of funds via MEV bot sandwich attacks:


  • In the swapExactTokensForTokens() function calls the amountOutMin is set to 0.


  • In the addLiquidity() function call the amountAMin and amountBMin are set to 1.


Code Location

function addLiquidity() internal {
    uint256 outputBal = IERC20(output).balanceOf(address(this));
    uint256 lp0Amt = outputBal / 2;
    uint256 lp1Amt = outputBal - lp0Amt;

    if (stable) {
        uint256 lp0Decimals = 10 ** IERC20Extended(lpToken0).decimals();
        uint256 lp1Decimals = 10 ** IERC20Extended(lpToken1).decimals();
        uint256 out0 = lpToken0 != output
            ? (ISolidlyRouter(unirouter).getAmountsOut(lp0Amt, outputToLp0Route)[outputToLp0Route.length] * 1e18) /
                lp0Decimals
            : lp0Amt;
        uint256 out1 = lpToken1 != output
            ? (ISolidlyRouter(unirouter).getAmountsOut(lp1Amt, outputToLp1Route)[outputToLp1Route.length] * 1e18) /
                lp1Decimals
            : lp1Amt;
        (uint256 amountA, uint256 amountB, ) = ISolidlyRouter(unirouter).quoteAddLiquidity(
            lpToken0,
            lpToken1,
            stable,
            factory,
            out0,
            out1
        );
        amountA = (amountA * 1e18) / lp0Decimals;
        amountB = (amountB * 1e18) / lp1Decimals;
        uint256 ratio = (((out0 * 1e18) / out1) * amountB) / amountA;
        lp0Amt = (outputBal * 1e18) / (ratio + 1e18);
        lp1Amt = outputBal - lp0Amt;
    }

    if (lpToken0 != output) {
        ISolidlyRouter(unirouter).swapExactTokensForTokens(
            lp0Amt,
            0,
            outputToLp0Route,
            address(this),
            block.timestamp
        );
    }

    if (lpToken1 != output) {
        ISolidlyRouter(unirouter).swapExactTokensForTokens(
            lp1Amt,
            0,
            outputToLp1Route,
            address(this),
            block.timestamp
        );
    }

    uint256 lp0Bal = IERC20(lpToken0).balanceOf(address(this));
    uint256 lp1Bal = IERC20(lpToken1).balanceOf(address(this));

    ISolidlyRouter(unirouter).addLiquidity(
        lpToken0,
        lpToken1,
        stable,
        lp0Bal,
        lp1Bal,
        1,
        1,
        address(this),
        block.timestamp
    );
}

function _chargeGreenFee() internal returns (uint256 _feeCharged) {
    uint256 outputBal = IERC20(output).balanceOf(address(this));
    (, , uint256 totalFeeInOutputToken) = getPerPoolGreenFee(outputBal);

    if (totalFeeInOutputToken == 0) {
        return 0;
    }

    address greenFeeToken = greenFeeConfig.greenFeeToken;

    uint256 greenFeeTokenBal;
    if (greenFeeToken != output) {  
        // swap output to greenFeeToken
        ISolidlyRouter(unirouter).swapExactTokensForTokens(
            totalFeeInOutputToken,
            0,
            outputToGreenFeeRoute,
            address(this),
            block.timestamp
        );
        greenFeeTokenBal = IERC20(greenFeeToken).balanceOf(address(this));
    } else {
        greenFeeTokenBal = totalFeeInOutputToken;
    }

    // Deposit all green token to green fee vault
    uint256 actualFeeDeposited = _depositGreenFee(greenFeeTokenBal);

    if (actualFeeDeposited != 0) {
        emit ContributedGreenFee(vault, actualFeeDeposited);
    }
}
BVSS
Recommendation

Dynamically calculate the amountOutMin value, to set the desired slippage tolerance that will not affect drastically the swap execution.

Remediation

RISK ACCEPTED: The KlimaDAO team made a business decision to accept the risk of this finding and not alter the contracts, stating:

Code is part of fork.

References

7.2 Unrestricted deadlines for swaps

// Low

Description

In the addLiquidity() and the _chargeGreenFee() functions of the StrategyAerodromeGaugeGreenFee contract, there are several interactions with the Aerodrome router that allow for unlimited deadlines.


When interacting with external AMM protocols such as Aerodrome, it is recommended to avoid setting the deadline parameter to the current block.timestamp.


A malicious block builder can withhold swap transactions and execute them later when it's advantageous for manipulating the price or offloading tokens onto the user at a disadvantageous price. Implementing a deadline parameter restricts the time frame during which an attacker can carry out such exploits.


Code Location

function addLiquidity() internal {
    uint256 outputBal = IERC20(output).balanceOf(address(this));
    uint256 lp0Amt = outputBal / 2;
    uint256 lp1Amt = outputBal - lp0Amt;

    if (stable) {
        uint256 lp0Decimals = 10 ** IERC20Extended(lpToken0).decimals();
        uint256 lp1Decimals = 10 ** IERC20Extended(lpToken1).decimals();
        uint256 out0 = lpToken0 != output
            ? (ISolidlyRouter(unirouter).getAmountsOut(lp0Amt, outputToLp0Route)[outputToLp0Route.length] * 1e18) /
                lp0Decimals
            : lp0Amt;
        uint256 out1 = lpToken1 != output
            ? (ISolidlyRouter(unirouter).getAmountsOut(lp1Amt, outputToLp1Route)[outputToLp1Route.length] * 1e18) /
                lp1Decimals
            : lp1Amt;
        (uint256 amountA, uint256 amountB, ) = ISolidlyRouter(unirouter).quoteAddLiquidity(
            lpToken0,
            lpToken1,
            stable,
            factory,
            out0,
            out1
        );
        amountA = (amountA * 1e18) / lp0Decimals;
        amountB = (amountB * 1e18) / lp1Decimals;
        uint256 ratio = (((out0 * 1e18) / out1) * amountB) / amountA;
        lp0Amt = (outputBal * 1e18) / (ratio + 1e18);
        lp1Amt = outputBal - lp0Amt;
    }

    if (lpToken0 != output) {
        ISolidlyRouter(unirouter).swapExactTokensForTokens(
            lp0Amt,
            0,
            outputToLp0Route,
            address(this),
            block.timestamp
        );
    }

    if (lpToken1 != output) {
        ISolidlyRouter(unirouter).swapExactTokensForTokens(
            lp1Amt,
            0,
            outputToLp1Route,
            address(this),
            block.timestamp
        );
    }

    uint256 lp0Bal = IERC20(lpToken0).balanceOf(address(this));
    uint256 lp1Bal = IERC20(lpToken1).balanceOf(address(this));

    ISolidlyRouter(unirouter).addLiquidity(
        lpToken0,
        lpToken1,
        stable,
        lp0Bal,
        lp1Bal,
        1,
        1,
        address(this),
        block.timestamp
    );
}

function _chargeGreenFee() internal returns (uint256 _feeCharged) {
    uint256 outputBal = IERC20(output).balanceOf(address(this));
    (, , uint256 totalFeeInOutputToken) = getPerPoolGreenFee(outputBal);

    if (totalFeeInOutputToken == 0) {
        return 0;
    }

    address greenFeeToken = greenFeeConfig.greenFeeToken;

    uint256 greenFeeTokenBal;
    if (greenFeeToken != output) {  
        // swap output to greenFeeToken
        ISolidlyRouter(unirouter).swapExactTokensForTokens(
            totalFeeInOutputToken,
            0,
            outputToGreenFeeRoute,
            address(this),
            block.timestamp
        );
        greenFeeTokenBal = IERC20(greenFeeToken).balanceOf(address(this));
    } else {
        greenFeeTokenBal = totalFeeInOutputToken;
    }

    // Deposit all green token to green fee vault
    uint256 actualFeeDeposited = _depositGreenFee(greenFeeTokenBal);

    if (actualFeeDeposited != 0) {
        emit ContributedGreenFee(vault, actualFeeDeposited);
    }
}
BVSS
Recommendation

Implement a more robust deadline parameter in order to restrict the time frame during which a trade can be executed.

Remediation

RISK ACCEPTED: The KlimaDAO team made a business decision to accept the risk of this finding and not alter the contracts, stating:

Code is part of fork.

References

7.3 Centralization risks

// Low

Description

Throughout the codebase, there are several roles that control critical contract configurations and operations. These roles have unrestricted power to modify core protocol parameters, pause functionality, upgrade contracts, and control user funds with no technical restrictions or safeguards in place. If these privileged roles are compromised or act maliciously, they could manipulate protocol parameters, halt operations, or withdraw user funds, effectively enabling rug pulls.


Particularly, the AutoRetireGreenVault, StrategyAerodromeGaugeGreen and GreenFeeManagerInitializable contracts inherit from the Ownable contract, which grants the owner unrestricted access to the contract's functions. The owner can change critical parameters, pause the contract, and withdraw funds from the contract without restrictions.


This centralization of power directly contradicts principles of decentralization and may put user deposits at significant risk.

BVSS
Recommendation

Several remedial strategies can be employed, including but not limited to: transitioning control to a multi-signature wallet setup for critical functions, establishing community-driven governance for decision-making on fund management, and/or integrating time locks.

Remediation

RISK ACCEPTED: The KlimaDAO team made a business decision to accept the risk of this finding and not alter the contracts.

References

7.4 Single step ownership transfer process

// Low

Description

The GreenFeeManagerInitializable contract and the StratFeeManagerInitializable inherits the OwnableUpgradeable contract implementation from OpenZeppelin's library and is used to restrict access to certain functions to the contract owner. The Ownable pattern allows the contract owner to transfer ownership to another address using the transferOwnership() function. However, the transferOwnership() function does not include a two-step process to transfer ownership.


In this regard, it is crucial that the address to which ownership is transferred is verified to be active and willing to assume ownership responsibilities. Otherwise, the contract could be locked in a situation where it is no longer possible to make administrative changes to it.


Additionally, the renounceOwnership() function allows renouncing to the owner permission. Renouncing ownership before transferring it would result in the contract having no owner, eliminating the ability to call privileged functions.

BVSS
Recommendation

Consider using OpenZeppelin's Ownable2StepUpgradeable contract over the OwnableUpgradeable implementation. Ownable2StepUpgradeable provides a two-step ownership transfer process, which adds an extra layer of security to prevent accidental ownership transfers.


Additionally, it is recommended that the owner cannot call the renounceOwnership() function to avoid losing ownership of the contract.

Remediation

RISK ACCEPTED: The KlimaDAO team made a business decision to accept the risk of this finding and not alter the contracts.

References

7.5 Precision loss due to division before multiplication

// Low

Description

The ratio calculation in the addLiquidity() function of the StrategyAerodromeGaugeGreen contract performs a division before multiplication. This could lead to an incorrect ratio calculation if precision loss occurs due to how Solidity handles division, rounding down to the nearest integer.


Code Location

uint256 ratio = (((out0 * 1e18) / out1) * amountB) / amountA;
BVSS
Recommendation

It is recommended to perform the multiplication before the division to avoid precision loss. For example, an equivalent calculation would be:

uint256 ratio = (out0 * 1e18 * amountB) / (out1 * amountA);
Remediation

SOLVED: The KlimaDAO team solved this finding in commit 619a499 by following the mentioned recommendation.

Remediation Hash
References

7.6 Potential reentrancy behavior when harvesting

// Low

Description

The _harvest() function of the StrategyAerodromeGaugeGreen contract makes several external calls for compounding earnings and charging performance fees before updating state. Although all external calls are made to trusted contracts, the sequence of getReward -> _chargeGreenFee -> addLiquidity -> deposit could leave the contract vulnerable to reentrancy. It is always recommended to implement the Checks-Effects-Interactions pattern to prevent unwanted reentrancy behavior.


Code Location

function _harvest(address callFeeRecipient) internal whenNotPaused {
    IAerodromeGauge(gauge).getReward(address(this));
    uint256 outputBal = IERC20(output).balanceOf(address(this));
    if (outputBal > 0) {
        // Given the output Bal is split into 1:1 ratio,
        // therefore we can charger the green fee on the outputBal
        // as a whole and then split the amounts into 1:1 ratio and add
        // liquidity to the AMM
        _chargeGreenFee();
        addLiquidity();

        uint256 wantHarvested = balanceOfWant();

        deposit();

        lastHarvest = block.timestamp;
        emit StratHarvest(msg.sender, wantHarvested, balanceOf());
    }
}
BVSS
Recommendation

Implement the Checks-Effects-Interactions pattern or use a reentrancy guard mechanism such as OpenZeppelin's nonReentrant modifier from the ReentrancyGuardUpgradeable contract to prevent reentrancy behavior.

Remediation

RISK ACCEPTED: The KlimaDAO team made a business decision to accept the risk of this finding and not alter the contracts, stating:

The likelihood of a reenterancy attack for the Harvest Function is negligible because 1. callFeeRecipient is not being used in the internal function and hence chances of it being involved in re-enterancy in this regard is none. 2. Another external call would be to Gauge. Now setting gauge value is an owner action and hence, likelihood of reenterancy is subdued.

References

7.7 Inconsistent use of SafeERC20 library functions

// Low

Description

Throughout the StrategyAerodromeGaugeGreen contract, there is consistent use of the SafeERC20 library to handle edge cases on ERC20 token that may no be fully compliant. However, in the retireStrat() function, the ERC20 token transfer is performed using the transfer() function directly.


This may lead to potential unexpected behavior if the token being transferred is not fully ERC20 compliant.


Code Location

function retireStrat() external {
    require(msg.sender == vault, "!vault");

    IAerodromeGauge(gauge).withdraw(balanceOfPool());

    uint256 wantBal = IERC20(want).balanceOf(address(this));
    IERC20(want).transfer(vault, wantBal);
}
BVSS
Recommendation

It is recommended to use the safeTransfer() function from the SafeERC20 library to handle the transfer of ERC20 tokens in the retireStrat() function.

Remediation

RISK ACCEPTED: The KlimaDAO team made a business decision to accept the risk of this finding and not alter the contracts, stating:

Code is part of fork.

References

7.8 Missing input validation

// Informational

Description

Throughout the codebase, there are several instances where an input address is assigned to a state variable without checking if the input address is address(0). Assigning address(0) to a state variable can lead to unexpected behavior of the system. Instances of this issue include:


  • StrategyAerodromeGaugeGreen.initialize()

  • StratFeeManagerInitializable.setVault()

  • StratFeeManagerInitializable.setUnirouter()

  • StratFeeManagerInitializable.setKeeper()

  • StratFeeManagerInitializable.setStrategist()

  • StratFeeManagerInitializable.setBeefyFeeRecipient()

  • StratFeeManagerInitializable.setBeefyFeeConfig()

BVSS
Recommendation

It is recommended to check if any input address is the address(0) before assigning it to a state variable to prevent unexpected behavior.

Remediation

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

References

7.9 Use of outdated libraries

// Informational

Description

Throughout the codebase, several OpenZeppelin contracts implementations are inherited and used. These contracts are used to implement access control, pausing functionality, ERC20 token standards, and more. However, some of the versions of these contracts are outdated and may contain vulnerabilities that may have been fixed in newer versions.


For more reference about OpenZeppelin contracts versions and their vulnerabilities, see https://security.snyk.io/package/npm/@openzeppelin%2Fcontracts

BVSS
Recommendation

Consider updating all OpenZeppelin contracts to the latest versions to benefit from the latest security patches and improvements.

Remediation

ACKNOWLEDGED: The KlimaDAO team made a business decision to acknowledge this finding and not alter the contracts, stating:

Part of Fork and it's a bigger undertaking to update to upgraded versions.

References

7.10 Missing check for possible division by zero

// Informational

Description

The ratio calculation in the addLiquidity() function of the StrategyAerodromeGaugeGreen contract performs two divisions without explictly ensuring that none of the denominators are 0. This could lead to a division by zero type error. For example, this scenario could appear if the getAmountsOut() function of the Aerodrome router returns 0.

BVSS
Recommendation

It is recommended to add an explicit check to ensure that the denominator is not zero before performing any division.

Remediation

ACKNOWLEDGED: The KlimaDAO team made a business decision to acknowledge this finding and not alter the contracts, stating:

Part of Fork + We don't plan to onboard stable liquidity.

References

7.11 Possible unauthorized movement of funds

// Informational

Description

The deposit() function in the StrategyAerodromeGaugeGreen contract allows for depositing all current want tokens to the Aerodrome Gauge external contract. However, this function can be triggered by anyone, which could lead to unexpected behavior if the correct execution flow is expected to deposit the funds in the harvest() function only:

function deposit() public whenNotPaused {
    uint256 wantBal = IERC20(want).balanceOf(address(this));

    if (wantBal > 0) {
        IAerodromeGauge(gauge).deposit(wantBal, address(this));
        emit Deposit(balanceOf());
    }
}
BVSS
Recommendation

Consider adding access control to the deposit() function to allow only specific addresses to trigger the function, for example the Vault contract. Additionally, document exhaustively the expected behavior of the function and the expected execution flow.

Remediation

PARTIALLY SOLVED: The KlimaDAO team partially solved this finding in commit 6cf6ace by adding information to the deposit() NASTPEC comments explaining its functionality.

Remediation Hash
References

7.12 Inconsistency in harvest mechanism

// Informational

Description

The harvest() mechanism in the StrategyAerodromeGaugeGreen contract allows to compound earnings and charges performance fee. However, there is no minimum harvest threshold, which could lead to inefficient harvesting when triggered with low amounts.

Additionally, the callFeeRecipient parameter is not used in the function, which may indicate a potential inconsistency in the function's design and lead to confusion since both the harvest() and harvest(address callFeeRecipient) functions will provide the same functionality.


Code Location

function _harvest(address callFeeRecipient) internal whenNotPaused {
    IAerodromeGauge(gauge).getReward(address(this));
    uint256 outputBal = IERC20(output).balanceOf(address(this));
    if (outputBal > 0) {
        // Given the output Bal is split into 1:1 ratio,
        // therefore we can charger the green fee on the outputBal
        // as a whole and then split the amounts into 1:1 ratio and add
        // liquidity to the AMM
        _chargeGreenFee();
        addLiquidity();

        uint256 wantHarvested = balanceOfWant();

        deposit();

        lastHarvest = block.timestamp;
        emit StratHarvest(msg.sender, wantHarvested, balanceOf());
    }
}

function harvest() external virtual {
    _harvest(tx.origin);
}

function harvest(address callFeeRecipient) external virtual {
    _harvest(callFeeRecipient);
}
BVSS
Recommendation

Consider adding a minimum harvest threshold to the harvest() function to prevent inefficient harvesting. Additionally, ensure that the callFeeRecipient parameter is used in the function to avoid confusion and maintain consistency in the function's design, or remove it if it is not necessary.

Remediation

ACKNOWLEDGED: The KlimaDAO team made a business decision to acknowledge this finding and not alter the contracts, stating:

Part of Fork: Acknowledge the capital inefficiency, but it only gets triggered when there's not enough daily rewards, or the pool size is small.

References

7.13 Ignored return values

// Informational

Description

Several functions in the StrategyAerodromeGaugeGreen contract make external calls that return values, but the return values are ignored. Ignoring return values can lead to unexpected behavior if the return values are used to determine the success or failure of the external calls. Instances of this issue are in:


  • StrategyAerodromeGaugeGreen._harvest()

  • StrategyAerodromeGaugeGreen._chargeGreenFee()

  • StrategyAerodromeGaugeGreen.addLiquidity()

Score
Recommendation

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

Remediation

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

References

7.14 Use of magic numbers

// Informational

Description

In the StrategyAerodromeGaugeGreenFee contract, there are several instances where magic numbers are used. Magic numbers are direct numerical or string values used in the code without any explanation or context. This practice can lead to code maintainability issues, potential errors, and difficulty in understanding the code's logic.

Score
Recommendation

To improve the code’s readability and facilitate refactoring, consider defining a constant for every magic number, giving it a clear and self-explanatory name. Consider adding an inline comment explaining how the magic numbers are calculated or why they are chosen for complex values.

Remediation

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

References

7.15 Unused components

// Informational

Description

In the StrategyAerodromeGaugeGreen contract, the IInterchainAutoRetireGreenVault interface is imported but never used. Additionally, the _checkFeeManager() function in the GreenFeeManagerInitializable contract is not used in the contract, as well as some errors in the GreenVaultErrorsLib are declared but not used in the AutoRetireGreenVault contract. Unused imports can be removed to improve code readability and maintainability.


Score
Recommendation

Consider removing unused imports, functions and errors to declutter the code and improve readability or implement the intended functionality.

Remediation

SOLVED: The KlimaDAO team solved this finding in commit 6cf6ace by following the mentioned recommendation.

Remediation Hash
References

7.16 Public functions can be marked external

// Informational

Description

Some functions in the StrategyAerodromeGaugeGreen contract are currently defined with the public visibility modifier, even though the functions are not called from within the contract, resulting in higher gas costs than necessary. Instances of this issue include:


  • StrategyAerodromeGaugeGreen.callReward()

  • StrategyAerodromeGaugeGreen.panic()

Score
Recommendation

Modify the public functions not used within the contracts with the external visibility modifier.

Remediation

SOLVED: The KlimaDAO team solved this finding in commit c3e53e6 by following the mentioned recommendation.

Remediation Hash
References

7.17 Floating pragma

// Informational

Description

The contracts in scope currently use floating pragma versions ^0.8.0 and ^0.8.23 which means that the code can be compiled by any compiler version that is greater than or equal to 0.8.0, or 0.8.23 respectively, and less than 0.9.0.


However, it is recommended that contracts should be deployed with the same compiler version and flags used during development and testing. Locking the pragma helps to ensure that contracts do not accidentally get deployed using another pragma. For example, an outdated pragma version might introduce bugs that affect the contract system negatively.

Score
Recommendation

Lock the pragma version to the same version used during development and testing.

Remediation

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

References

7.18 Use of revert strings over custom errors

// Informational

Description

Throughout the file in scope, there are several instances of use of revert strings over custom errors.


In Solidity development, replacing hard-coded revert message strings with the Error() syntax is an optimization strategy that can significantly reduce gas costs. Hard-coded strings, stored on the blockchain, increase the size and cost of deploying and executing contracts.


The Error() syntax allows for the definition of reusable, parameterized custom errors, leading to a more efficient use of storage and reduced gas consumption. This approach not only optimizes gas usage during deployment and interaction with the contract but also enhances code maintainability and readability by providing clearer, context-specific error information.

Score
Recommendation

Consider replacing all revert strings with custom errors. For example:

error ConditionNotMet();

if (!condition) revert ConditionNotMet();

For more reference, see here.

Remediation

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

References

7.19 Incomplete NATSPEC documentation

// Informational

Description

NATSPEC comments are essential for providing clear and concise explanations of the purpose and functionality of the code. They help developers understand the codebase, facilitate code maintenance, and improve overall code quality. However, throughout the contracts in scope, not all the functions and declared variables have clear NATSPEC documentation.


Particularly, the StrategyAerodromeGaugeGreenFee contract is missing NATSPEC comments for the functions and declared variables.

Score
Recommendation

Add NATSPEC comments to all functions and declared variables in the contracts to provide clear explanations of their purpose, inputs, outputs, and any other relevant information. This will help improve code readability, maintainability, and overall quality.

Remediation

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

References

8. Automated Testing

Static Analysis Report

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.

Output

The findings obtained as a result of the Slither scan were reviewed, and the majority were not included in the report because they were determined as false positives.

Slither results (1)Slither results (2)Slither results (3)Slither results (4)

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 2024. All rights reserved.