Halborn Logo

Core, Strategies and Periphery - GammaSwap Labs


Prepared by:

Halborn Logo

HALBORN

Last Updated 04/25/2024

Date of Engagement by: December 19th, 2022 - January 6th, 2023

Summary

75% of all REPORTED Findings have been addressed

All findings

16

Critical

0

High

1

Medium

2

Low

3

Informational

10


1. INTRODUCTION

GammaSwap allows users to provide liquidity into an existing AMM (e.g. Uniswap, Sushiswap, Balancer). At the same time, it allows other users to short LP tokens (borrow the Uniswap LP Token and retrieve the reserve tokens it represents) from users that have provided liquidity to an existing AMM though GammaSwap and therefore profit from the change in price. What is normally impermanent loss to liquidity providers becomes impermanent gains to liquidity shorters.

\client engaged Halborn to conduct a security audit on their smart contracts beginning on 2022-12-19 and ending on 2023-01-06. The security assessment was scoped to the smart contracts provided in the V1-core, V1-strategies and V1-periphery GitHub repositories. Commit hashes and further details can be found in the Scope section of this report.

2. AUDIT SUMMARY

The team at Halborn was provided 3 weeks for the engagement and assigned 3 full-time security engineer/engineers to audit the security of the smart contracts in scope. The security engineers are blockchain and smart contract security experts with advanced penetration testing and smart contract hacking skills, and deep knowledge of multiple blockchain protocols.

The purpose of the audits is to:

    • Ensure that smart contract functions operate as intended

    • Identify potential security issues with the smart contracts

In summary, Halborn identified some improvements to reduce the likelihood and impact of risks, which were mostly addressed by \client. The main ones are the following:

    1. All functions with mathematical operations should have sanity checks against division by zero findings

    2. Token decimals should be considered during price/fee calculations

    3. Other chain specifications should be considered

3. TEST APPROACH & METHODOLOGY

Halborn performed a combination of manual and automated security testing to balance efficiency, timeliness, practicality, and accuracy in regard to the scope of this audit. While manual testing is recommended to uncover flaws in logic, process, and implementation; automated testing techniques help enhance coverage of the code and can quickly identify items that do not follow the security best practices. The following phases and associated tools were used during the audit:

    • Research into architecture and purpose

    • Smart contract manual code review and walkthrough

    • Graphing out functionality and contract logic/connectivity/functions (solgraph)

    • Manual assessment of use and safety for the critical Solidity variables and functions in scope to identify any arithmetic related vulnerability classes

    • Manual testing by custom scripts

    • Scanning of solidity files for vulnerabilities, security hot-spots or bugs. (MythX)

    • Static Analysis of security for scoped contract, and imported functions. (Slither)

    • Testnet deployment (Brownie, Foundry)

4. SCOPE

The security assessment was scoped to the following smart contracts on these repositories:

\begin{enumerate} \item v1-core Repository: \href{https://github.com/gammaswap/v1-core}{v1-core main branch} \item Commit ID: \href{https://github.com/gammaswap/v1-core/tree/dbda72a563622afda678e8373f4a3e0cd091bb43}{dbda72a563622afda678e8373f4a3e0cd091bb43} \item Smart contracts in scope: \begin{itemize} \item contracts/pools/CPMMGammaPool.sol \item contracts/libraries/LibStorage.sol \item contracts/libraries/AddressCalculator.sol \item contracts/storage/AppStorage.sol \item contracts/GammaPoolFactory.sol \item contracts/base/AbstractGammaPoolFactory.sol \item contracts/base/GammaPoolERC4626.sol \item contracts/base/GammaPool.sol \item contracts/base/GammaPoolERC20.sol \end{itemize} \end{enumerate}

\begin{enumerate} \item v1-strategies Repository: \href{https://github.com/gammaswap/v1-strategies}{v1-strategies main branch} \item Commit ID: \href{https://github.com/gammaswap/v1-strategies/tree/f60285582b2b1178e36a8ebc8dddc6deaeb5c7f8}{f60285582b2b1178e36a8ebc8dddc6deaeb5c7f8} \item Smart contracts in scope: \begin{itemize} \item contracts/strategies/cpmm/CPMMBaseLongStrategy.sol \item contracts/strategies/cpmm/CPMMLongStrategy.sol \item contracts/strategies/cpmm/CPMMShortStrategy.sol \item contracts/strategies/cpmm/CPMMBaseStrategy.sol \item contracts/strategies/cpmm/CPMMLiquidationStrategy.sol \item contracts/strategies/base/LongStrategy.sol \item contracts/strategies/base/BaseLongStrategy.sol \item contracts/strategies/base/BaseStrategy.sol \item contracts/strategies/base/LiquidationStrategy.sol \item contracts/strategies/base/ShortStrategy.sol \item contracts/strategies/base/ShortStrategyERC4626.sol \item contracts/rates/LogDerivativeRateModel.sol \item contracts/rates/LinearKinkedRateModel.sol \end{itemize} \end{enumerate}

\begin{enumerate} \item v1-periphery Repository: \href{https://github.com/gammaswap/v1-periphery}{v1-periphery main branch} \item Commit ID: \href{https://github.com/gammaswap/v1-periphery/tree/1e28654c4ed123f543ef22927e26630929ca4680}{1e28654c4ed123f543ef22927e26630929ca4680} \item Smart contracts in scope: \begin{itemize} \item contracts/base/GammaPoolERC721.sol \item contracts/base/Transfers.sol \item contracts/PositionManager.sol \item contracts/storage/AppStorage.sol \item contracts/interfaces/ITransfers.sol \item contracts/interfaces/ISendTokensCallback.sol \item contracts/interfaces/IPositionManager.sol \end{itemize} \end{enumerate}

The minor code differences mentioned in the following commits and implemented during the audit were also reviewed:

Out-of-Scope Changes:

    • Balancer Contracts

5. RISK METHODOLOGY

Vulnerabilities or issues observed by Halborn are ranked based on the risk assessment methodology by measuring the LIKELIHOOD of a security incident and the IMPACT should an incident occur. This framework works for communicating the characteristics and impacts of technology vulnerabilities. The quantitative model ensures repeatable and accurate measurement while enabling users to see the underlying vulnerability characteristics that were used to generate the Risk scores. For every vulnerability, a risk level will be calculated on a scale of 5 to 1 with 5 being the highest likelihood or impact.
RISK SCALE - LIKELIHOOD
  • 5 - Almost certain an incident will occur.
  • 4 - High probability of an incident occurring.
  • 3 - Potential of a security incident in the long term.
  • 2 - Low probability of an incident occurring.
  • 1 - Very unlikely issue will cause an incident.
RISK SCALE - IMPACT
  • 5 - May cause devastating and unrecoverable impact or loss.
  • 4 - May cause a significant level of impact or loss.
  • 3 - May cause a partial impact or loss to many.
  • 2 - May cause temporary impact or loss.
  • 1 - May cause minimal or un-noticeable impact.
The risk level is then calculated using a sum of these two values, creating a value of 10 to 1 with 10 being the highest level of security risk.
Critical
High
Medium
Low
Informational
  • 10 - CRITICAL
  • 9 - 8 - HIGH
  • 7 - 6 - MEDIUM
  • 5 - 4 - LOW
  • 3 - 1 - VERY LOW AND INFORMATIONAL

6. SCOPE

Out-of-Scope: New features/implementations after the remediation commit IDs.

7. Assessment Summary & Findings Overview

Critical

0

High

1

Medium

2

Low

3

Informational

10

Impact x Likelihood

HAL-03

HAL-10

HAL-06

HAL-04

HAL-01

HAL-11

HAL-08

HAL-02

HAL-05

HAL-07

HAL-09

HAL-12

HAL-13

HAL-14

HAL-15

HAL-16

Security analysisRisk levelRemediation Date
INITIAL LP DEPOSIT IS IMPOSSIBLE DUE TO MISCALCULATIONHighSolved - 01/19/2023
INCORRECT INVARIANT FACTOR CALCULATION MAY LEAD TO A LOSS OF ACCRUED FUNDSMediumSolved - 01/19/2023
CALLING THE BATCHLIQUIDATIONS FUNCTION WITH TOKENID 0 ALWAYS REVERTSMediumNot Applicable
CENTRALIZATION RISKLowRisk Accepted
TOKEN SWAPPING CAN FAIL DUE TO DIVISION BY ZEROLowNot Applicable
MISSING ZERO ADDRESS CHECKSLowSolved - 01/23/2023
FIRST LIQUIDITY PROVIDER LOSES FUNDS DUE TO ROUNDING ISSUEInformational-
INCOMPATIBILITY WITH FEE-ON-TRANSFER OR DEFLATIONARY TOKENSInformational-
LOAN CALCULATION CAN BE MISLEADING FOR DIFFERENT CHAINS DUE TO HARDCODED VARIABLESInformational-
LACK OF ZERO AMOUNT CHECK MAY LEAD DIVISION BY ZEROInformational-
MISSING UPPER/LOWER BOUND CHECKSInformationalSolved - 01/23/2023
FOR LOOP OPTIMIZATIONSInformationalSolved - 01/23/2023
THE IMMUTABLE KEYWORD COSTS LESS GAS FOR CONSTANT VARIABLESInformationalSolved - 01/23/2023
UNUSED IMPORTSInformationalSolved - 01/23/2023
MISSING NATSPEC DOCUMENTATIONInformationalSolved - 02/14/2023
OPEN TODOsInformationalSolved - 01/23/2023

8. Findings & Tech Details

8.1 INITIAL LP DEPOSIT IS IMPOSSIBLE DUE TO MISCALCULATION

// High

Description

The _depositNoPull() function in the BaseStrategy contract does not work properly. The first deposit is reverted with the ZeroAmount() error.

In the _depositNoPull() function, the updateIndex() internal function is invoked with accFeeIndex, lastFeeIndex and lastCFMMFeeIndex call parameters. If the value of the s.BORROWED_INVARIANT variable is positive, then some LP tokens are minted to developers, since the protocol charges a handling fee. It has been observed however that the contract also tries to mint LP tokens to developers even if the s.BORROWED_INVARIANT is zero. In this case, the devShares variable returns zero since there are no deposits in the contract and s.totalSupply is equal to zero, which prevents users from depositing funds in the contract.

First Deposit Fails - PoC

function test_depositNoPullPoC() public {
        vm.roll(16392000 + 1); // deployment block + 1 
        _addLiquidityWithUniRouter(deployer, address(usdt), address(weth), 50 * 1e6, 10 * 1e18);

        vm.startPrank(deployer);

        usdt.transfer(user1, 5 * 1e6);
        weth.transfer(user1, 2 * 1e18);

        usdt.transfer(user2, 6 * 1e6);
        weth.transfer(user2, 10 * 1e18);


        uint256 loanId = positionManager.createLoan(1, address(cfmm_usdt_weth), deployer, type(uint).max);
        uint256 loanId2 = positionManager.createLoan(1, address(cfmm_usdt_weth), user2, type(uint).max);
        positionManager.transferFrom(deployer, user1, loanId);

        IPositionManager.DepositWithdrawParams memory depositData = IPositionManager.DepositWithdrawParams({
            protocolId: 1,
            cfmm: address(cfmm_usdt_weth),
            to: user1,
            lpTokens: cfmm_usdt_weth.balanceOf(deployer),
            deadline: 99999
        });

        cfmm_usdt_weth.approve(address(positionManager), cfmm_usdt_weth.balanceOf(deployer));

        positionManager.depositNoPull(depositData);

        vm.stopPrank();
    }

Screenshot:

Code Location

BaseStrategy

BaseStrategy

function updateIndex() internal virtual returns(uint256 accFeeIndex, uint256 lastFeeIndex, uint256 lastCFMMFeeIndex) {
    lastCFMMFeeIndex = updateCFMMIndex();
    lastFeeIndex = updateFeeIndex(lastCFMMFeeIndex);
    accFeeIndex = updateStore(lastFeeIndex);
    if(s.BORROWED_INVARIANT >= 0) {
        mintToDevs(lastFeeIndex, lastCFMMFeeIndex);
    }
}

BaseStrategy

BaseStrategy

function _mint(address account, uint256 amount) internal virtual {
    if(amount == 0) {
        revert ZeroAmount();
    }
    s.totalSupply += amount;
    s.balanceOf[account] += amount;
    emit Transfer(address(0), account, amount);
}
Score
Impact: 3
Likelihood: 5
Recommendation

SOLVED: This finding was identified in a live code walkthrough jointly by the GammaSwap team and the Halborn team, and the existence of the finding was confirmed by the Halborn team.

The greater than or equal to (>=) relation was replaced with greater than (>) relation.

Commit ID: v1-strategies::6f6a7ba1f0fe8b9d7a7cb9b756ef5b3e6bfa55ab

8.2 INCORRECT INVARIANT FACTOR CALCULATION MAY LEAD TO A LOSS OF ACCRUED FUNDS

// Medium

Description

The invariant factor formula makes use of the number of decimals of the first token in token pair. The result of this calculation is further used in multiple places across the contracts. For example, when you borrow liquidity from a GammaSwap pool, the protocol calculates that factor to update some storage variables such as lastFeeIndex and BORROWED_INVARIANT. Instead of focusing on s.decimal[0] only, both decimals should be considered.

As a result, calculating this variable based on a wrong number of decimals affects borrowed/repaid liquidity and many storage variables in the protocol.

Invariant Factor PoC

// replace the s.decimals[0] variable in the getInvariantFactor() with 1e18 and 1e6 to see difference.
function test_BorrowAndRepayLifeCycleTask01() public {
    vm.roll(16392000 + 1200);
    _addLiquidityWithUniRouter(deployer, address(usdt), address(weth), 10 * 1e6, 5 * 1e18);

    vm.startPrank(deployer);

    usdt.transfer(user1, 5 * 1e6);
    weth.transfer(user1, 2 * 1e18);

    usdt.transfer(user2, 6 * 1e6);
    weth.transfer(user2, 10 * 1e18);


    uint256 loanId = positionManager.createLoan(1, address(cfmm_usdt_weth), deployer, type(uint).max);
    uint256 loanId2 = positionManager.createLoan(1, address(cfmm_usdt_weth), user2, type(uint).max);
    positionManager.transferFrom(deployer, user1, loanId);

    IPositionManager.DepositWithdrawParams memory depositData = IPositionManager.DepositWithdrawParams({
        protocolId: 1,
        cfmm: address(cfmm_usdt_weth),
        to: user1,
        lpTokens: cfmm_usdt_weth.balanceOf(deployer),
        deadline: 99999
    });

    cfmm_usdt_weth.approve(address(positionManager), cfmm_usdt_weth.balanceOf(deployer));

    positionManager.depositNoPull(depositData);

    vm.stopPrank();

    _addLiquidityWithUniRouter(user1, address(usdt), address(weth), 1e6, 1e18);

    vm.startPrank(user1);

    uint256[] memory amountsDesired = new uint256[](2);
    amountsDesired[0] = 1e18;
    amountsDesired[1] = 1e6;

    IPositionManager.AddRemoveCollateralParams memory increaseCollData = IPositionManager.AddRemoveCollateralParams({
        protocolId: 1,
        cfmm: address(cfmm_usdt_weth),
        to: user1,
        tokenId: loanId,
        deadline: 99999,
        amounts: amountsDesired
    });

    usdt.approve(address(positionManager), amountsDesired[1]); 
    weth.approve(address(positionManager), amountsDesired[0]);

    vm.roll(16392000 + 1252);
    positionManager.increaseCollateral(increaseCollData);

    vm.stopPrank();

    vm.startPrank(user2);

    increaseCollData.tokenId = loanId2;
    increaseCollData.to = user2;

    amountsDesired[0] = 1e18;
    amountsDesired[1] = 1e6;

    increaseCollData.amounts = amountsDesired;

    usdt.approve(address(positionManager), amountsDesired[1]); 
    weth.approve(address(positionManager), amountsDesired[0]);

    vm.roll(16392000 + 1304);
    positionManager.increaseCollateral(increaseCollData);

    vm.stopPrank();

    vm.startPrank(user1);

    IPositionManager.BorrowLiquidityParams memory borrowLqtyData = IPositionManager.BorrowLiquidityParams({
        protocolId: 1,
        cfmm: address(cfmm_usdt_weth),
        to: user1,
        tokenId: loanId,
        lpTokens: uint256(1e12) * 800 / 1000,
        deadline: 99999,
        minBorrowed: new uint256[](2)
    });

    positionManager.borrowLiquidity(borrowLqtyData);

    vm.roll(16392000 + 1340);

    amountsDesired[0] = 1e18;
    amountsDesired[1] = 1e6;

    increaseCollData.amounts = amountsDesired;
    vm.stopPrank();

    vm.prank(user2);
    positionManager.decreaseCollateral(increaseCollData);


    IPositionManager.RepayLiquidityParams memory repayData = IPositionManager.RepayLiquidityParams({
        protocolId: 1,
        cfmm: address(cfmm_usdt_weth),
        to: user1,
        tokenId: loanId,
        liquidity: 1000000 / 2,
        deadline: 99999,
        minRepaid: new uint256[](2)
    });

    vm.stopPrank();

    vm.startPrank(deployer);
    usdt.transfer(address(cfmm_usdt_weth), 10 * 1e6);
    weth.transfer(address(cfmm_usdt_weth), 5 * 1e18);
    vm.stopPrank();
}

Screenshot:

Code Location

BaseStrategy

BaseStrategy.sol

function getInvariantFactor() internal virtual override view returns(uint256) {
    return 10 ** s.decimals[0];
}

AbstractRateModel

AbstractRateModel.sol

function calcUtilizationRate(uint256 lpInvariant, uint256 borrowedInvariant) internal virtual view returns(uint256) {
    uint256 totalInvariant = lpInvariant + borrowedInvariant;
    if(totalInvariant == 0)
        return 0;

    return borrowedInvariant * getInvariantFactor() / totalInvariant;
}
Score
Impact: 4
Likelihood: 3
Recommendation

SOLVED: This finding was identified in a code walkthrough jointly by the GammaSwap team and the Halborn team, and the existence of the finding was confirmed by the Halborn team. The invariant factor is now calculated as 10**18.

Commit ID: v1-strategies::85864193924b7d1299dd99a27ded752c807ae2cb

8.3 CALLING THE BATCHLIQUIDATIONS FUNCTION WITH TOKENID 0 ALWAYS REVERTS

// Medium

Description

The _batchLiquidations function implemented in the LiquidationStrategy contract is designed to perform more than one liquidation in a go. The tokenId > 0 check on the payLoanAndRefundLiquidator function assures that users can use 0 as tokenId for batch liquidation operation. However, it is not possible to use 0 as tokenId in the _batchLiquidations function.

The _batchLiquidations function makes an internal call to the sumLiquidity function. During the calculation of the liquidity variable, the execution is reverted with the Division or modulo by 0 error since the _loan.rateIndex is also zero for tokenId 0.

Code Location

LiquidationStrategy.sol

function sumLiquidity(uint256[] calldata tokenIds) internal virtual returns(uint256 liquidityTotal, uint256 collateralTotal, uint256 lpTokensPrincipalTotal, uint128[] memory tokensHeldTotal) {
    address[] memory tokens = s.tokens;
    uint128[] memory tokensHeld;
    address cfmm = s.cfmm;
    tokensHeldTotal = new uint128[](tokens.length);
    (uint256 accFeeIndex,,) = updateIndex();
    for(uint256 i = 0; i < tokenIds.length; i++) {
        LibStorage.Loan storage _loan = s.loans[tokenIds[i]];
        uint256 liquidity = uint128((_loan.liquidity * accFeeIndex) / _loan.rateIndex);
        tokensHeld = _loan.tokensHeld;
        lpTokensPrincipalTotal = lpTokensPrincipalTotal + _loan.lpTokens;
        _loan.liquidity = 0;
        _loan.initLiquidity = 0;
        _loan.rateIndex = 0;
        _loan.lpTokens = 0;
        uint256 collateral = calcInvariant(cfmm, tokensHeld);
        canLiquidate(collateral, liquidity, 950);
        collateralTotal = collateralTotal + collateral;
        liquidityTotal = liquidityTotal + liquidity;
        for(uint256 j = 0; j < tokens.length; j++) {
            tokensHeldTotal[j] = tokensHeldTotal[j] + tokensHeld[j];
            _loan.tokensHeld[j] = 0;
        }
    }
}
Score
Impact: 3
Likelihood: 3
Recommendation

NOT APPLICABLE: The GammaSwap team confirmed that throwing the "Division or modulo by zero" error is the intended behavior.

8.4 CENTRALIZATION RISK

// Low

Description

The extra condition in the if statement described in the Code Location section poses a centralization risk. The isRestricted function is designed to check if a protocolId is restricted. However, the _owner of the contract is excluded from this control. This increases the centralization of the contract.

Code Location

GammaPoolFactory

GammaPoolFactory.sol

function isRestricted(uint16 protocolId, address _owner) internal virtual view {
    if(isProtocolRestricted[protocolId] == true && msg.sender != _owner) {
        revert ProtocolRestricted();
    }
}
Score
Impact: 3
Likelihood: 2
Recommendation

RISK ACCEPTED: The GammaSwap team accepted the risk, and they confirmed a multisig wallet will be the owner of the contract.

8.5 TOKEN SWAPPING CAN FAIL DUE TO DIVISION BY ZERO

// Low

Description

There is an edge case scenario of token swapping operation which leads to the division by zero error. The beforeSwapTokens function in the CPMMBaseLongStrategy contract is a function which calculates the exact in and out amounts of the swap operation. If amountIn and reserveIn parameters of calcAmtOut() function are equal, then the denominator is equal to zero. As a result, the swapping operation fails in this case.

Code Location

CPMMBaseLongStrategy.sol

function calcAmtOut(uint256 amountIn, uint256 reserveOut, uint256 reserveIn) internal view returns (uint256) {
    if(reserveOut == 0 || reserveIn == 0) {
        revert ZeroReserves();
    }
    uint256 denominator = (reserveIn - amountIn) * tradingFee1;
    return (reserveOut * amountIn * tradingFee2 / denominator) + 1;
}
Score
Impact: 2
Likelihood: 3
Recommendation

NOT APPLICABLE: The GammaSwap team confirmed the delta of reserveIn and amountIn will not be equal to 0 for Uniswap swaps. Therefore, reaching the "Division by zero" revert is impossible in this edge case.

8.6 MISSING ZERO ADDRESS CHECKS

// Low

Description

Contracts in-scope are missing address validation in constructors and setter functions. It is possible to configure the address(0), which may cause issues during execution.

For instance, if address(0) is passed to setFeeToSetter function, it will not be possible to change this address in the future.

Code Location

Following functions are not validating, that given address is different from zero:

  • PositionManager.sol, #55 - payee
  • GammaPoolFactory.sol, #95 - setFeeToSetter
  • ShortStrategy.sol, #36 - to
  • ShortStrategy.sol, #68 - to
  • BaseLongStrategy.sol, #34 - to
Score
Impact: 3
Likelihood: 1
Recommendation

SOLVED: This finding was solved by the GammaSwap team by requiring the payee and setFeeToSetter variables not to be equal to the zero address in the PositionManager and GammaPoolFactory contracts.

This finding is not applicable to the (to) variables because the GammaSwap team will allow users to send their funds to address(0) in order to burn assets.

8.7 FIRST LIQUIDITY PROVIDER LOSES FUNDS DUE TO ROUNDING ISSUE

// Informational

Description
Finding description placeholder
Score
Impact:
Likelihood:

8.8 INCOMPATIBILITY WITH FEE-ON-TRANSFER OR DEFLATIONARY TOKENS

// Informational

Description
Finding description placeholder
Score
Impact:
Likelihood:

8.9 LOAN CALCULATION CAN BE MISLEADING FOR DIFFERENT CHAINS DUE TO HARDCODED VARIABLES

// Informational

Description
Finding description placeholder
Score
Impact:
Likelihood:

8.10 LACK OF ZERO AMOUNT CHECK MAY LEAD DIVISION BY ZERO

// Informational

Description
Finding description placeholder
Score
Impact:
Likelihood:

8.11 MISSING UPPER/LOWER BOUND CHECKS

// Informational

Description

The _baseRate, _factor, _maxApy parameters of the LogDerivativeRateModel contract and _baseRate, _slope1, _slope2, _optimalUtilRate of the LinearKinkedRateModel contract are not checked to fall in the expected ranges. Some function calls might be therefore reverted due to possible arithmetic overflow issues, since no sanity checks are performed.

Code Location

LogDerivativeRateModel

LogDerivativeRateModel

constructor(uint64 _baseRate, uint80 _factor, uint80 _maxApy) {
    baseRate = _baseRate;
    factor = _factor;
    maxApy = _maxApy;
}

LinearKinkedRateModel

LinearKinkedRateModel

constructor(uint64 _baseRate, uint64 _optimalUtilRate, uint64 _slope1, uint64 _slope2) {
    baseRate = _baseRate;
    optimalUtilRate = _optimalUtilRate;
    slope1 = _slope1;
    slope2 = _slope2;
}
Score
Impact: 2
Likelihood: 1
Recommendation

SOLVED: The GammaSwap team fixed this issue by adding sanity checks in the above contracts.

Commit ID: v1-strategies::db000bdedb35ce0c7c6e93f892f552c21be286d4

8.12 FOR LOOP OPTIMIZATIONS

// Informational

Description

In for loops, the variable i is incremented using i++. It is known that in loops, using ++i costs less gas per iteration than i++.

It has also been detected that some uint256 variables are initialized to 0. These variables are already initialized to 0 by default, so uint256 i = 0 would reassign the 0 to i which wastes gas.

In addition, starting from pragma 0.8.0, adding unchecked keyword for arithmetical operations can reduce gas usage on contracts where underflow/underflow is unrealistic.

Score
Impact: 1
Likelihood: 1
Recommendation

8.13 THE IMMUTABLE KEYWORD COSTS LESS GAS FOR CONSTANT VARIABLES

// Informational

Description

The _name and _symbol variables in the GammaPoolERC721 contract were designed to be set only once. The immutable keyword consumes less gas. It might be useful to declare these variables as immutable to optimize gas usage in the protocol.

Code Location

GammaPoolERC721.sol

string private _name;

// Token symbol
string private _symbol;
Score
Impact: 1
Likelihood: 1
Recommendation

SOLVED: This finding was solved by the GammaSwap team by moving the _name and _symbol variables to the PositionManager contract and declaring them constant.

Commit ID: v1-periphery::4e2b377de6888c1f7845cbd9485605ecfca053f1

8.14 UNUSED IMPORTS

// Informational

Description

There are a few unused imports on the code base. These imports should be cleaned up from the code if they have no purpose. Clearing these imports will increase the readability of contracts.

Code Location

Score
Impact: 1
Likelihood: 1
Recommendation

SOLVED: This finding was solved by the GammaSwap team by removing unused imports from the above contracts.

Commit ID: v1-strategies::8e9aa10f9a6ece1c4fb0b8b1b25a1f8e7644bcc0

8.15 MISSING NATSPEC DOCUMENTATION

// Informational

Description

Solidity contracts can use a special form of comments to provide rich documentation for functions, return variables and more. This special form is named the Ethereum Natural Language Specification Format (NatSpec).

Score
Impact: 1
Likelihood: 1
Recommendation

SOLVED: This finding was fixed by the GammaSwap team by adding natspecs to all contracts.

v1-core::5dd28bb25f77bf7b3360b7420507e16688692f60

8.16 OPEN TODOs

// Informational

Description

Open TO-DOs can point to architecture or programming issues that still need to be resolved. Often these kinds of comments indicate areas of complexity or confusion for developers. This provides value and insight to an attacker who aims to cause damage to the protocol.

Code Location

References:

Score
Impact: 1
Likelihood: 1
Recommendation

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

Results

Core contracts:

Slither Result - core 1Slither Result - core 2Slither Result - core 3

Periphery contracts:

Slither Result - periphery 1Slither Result - periphery 2Slither Result - periphery 3

Strategies contracts:

Slither Result - strategies 1Slither Result - strategies 2Slither Result - strategies 3Slither Result - strategies 4

AUTOMATED SECURITY SCAN

Description

Halborn used automated security scanners to assist with detection of well-known security issues and to identify low-hanging fruits on the targets for this engagement. Among the tools used was MythX, a security analysis service for Ethereum smart contracts. MythX performed a scan on the smart contracts and sent the compiled results to the analyzers in order to locate any vulnerabilities.

Results

MythX Result

The findings obtained as a result of the MythX scan were examined and the findings were not included in the report because they were false positive.

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.