Halborn Logo

Protocol - Irrigation


Prepared by:

Halborn Logo

HALBORN

Last Updated 04/25/2024

Date of Engagement by: June 23rd, 2023 - August 4th, 2023

Summary

100% of all REPORTED Findings have been addressed

All findings

9

Critical

2

High

0

Medium

4

Low

1

Informational

2


1. INTRODUCTION

Irrigation Protocol engaged Halborn to conduct a security assessment on their smart contracts beginning on June 23rd, 2023 and ending on August 4th, 2023. The security assessment was scoped to the smart contracts provided in the following GitHub repositories:

2. ASSESSMENT SUMMARY

The team at Halborn was provided six weeks for the engagement and assigned a full-time security engineer to verify the security of the smart contracts. The security engineer is a blockchain and smart-contract security expert with advanced penetration testing, smart-contract hacking, and deep knowledge of multiple blockchain protocols.

The purpose of this assessment is to:

    • Ensure that smart contract functions operate as intended.

    • Identify potential security issues with the smart contracts.

In summary, Halborn identified some security risks that were mostly addressed by the Irrigation Protocol team.

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

    • 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. (Foundry)

4. SCOPE

**1. IN-SCOPE TREE & COMMIT : **

The security assessment was scoped to the following smart contracts:

GitHub repository: IrrigationProtocol/irrigation-contracts-diamond Commit ID: 159927ff1e8f8e212cd09894c29c1019e5d417f6 Remediation Commit ID: 1b9420fb77d856bb57a35e947ae255b035e3037c Smart contracts in scope:

    • SprinklerUpgradeable.sol

    • WaterTowerUpgradeable.sol

    • TrancheBondUpgradeable.sol

    • AuctionUpgradeable.sol

    • ERC1155WhitelistUpgradeable.sol

    • WaterCommonUpgradeable.sol

    • PriceOracleUpgradeable.sol

    • PodsOracleUpgradeable.sol

    • WaterUpgradeable.sol

    • SprinklerStorage.sol

    • WaterTowerStorage.sol

    • TrancheBondStorage.sol

    • AuctionStorage.sol

    • ERC1155WhitelistStorage.sol

    • WaterCommonStorage.sol

    • PriceOracleStorage.sol

    • IrrigationDiamond.sol

    • Diamond.sol

    • DiamondCutFacet.sol

    • DiamondLoupeFacet.sol

    • EIP2535Initializable.sol

    • IrrigationAccessControl.sol

    • OwnershipFacet.sol

    • FullMath.sol

    • PodTransferHelper.sol

    • LibPrice.sol

    • BeanPriceOracle.sol

    • ChainlinkOracle.sol

    • UniswapV3Twap.sol

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

5.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:
EXPLOITABILIY 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

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

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

6. SCOPE

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

7. Assessment Summary & Findings Overview

Critical

2

High

0

Medium

4

Low

1

Informational

2

Security analysisRisk levelRemediation Date
SPRINKLERUPGRADEABLE CONTRACT CAN BE DRAINED THROUGH THE EXCHANGETOWATER FUNCTIONCriticalSolved - 08/21/2023
AUCTIONS AND BIDS COULD BE STUCK PERMANENTLY IN THE AUCTIONUPGRADEABLE CONTRACT IF A BID IS PLACED BY A BLACKLISTED USDC/USDT USERCriticalSolved - 08/21/2023
BIDS CAN BE DOS'ED BY PLACING A VERY HIGH BID ON A VERY SMALL BIDAMOUNTMediumSolved - 08/21/2023
LATESTANSWER CALL MAY RETURN STALE RESULTSMediumSolved - 08/21/2023
USER COULD CANCEL THE REST OF BIDS OF AN AUCTION BY DOING 200 DIFFERENT BIDSMediumSolved - 08/21/2023
SWAPETHFORWATER CALL CAN BE SANDWICHEDMediumSolved - 08/21/2023
AUTOIRRIGATECALL WILL ALWAYS REVERT WITH OVERFLOW IF IT'S CALLED WITH THE FULL REWARDAMOUNTLowRisk Accepted - 08/21/2023
LACK OF A DOUBLE-STEP TRANSFEROWNERSHIP PATTERNInformationalAcknowledged - 08/21/2023
CALLING SETMIDDLEASSET WOULD CAUSE THE IRRIGATION BONUS TO ALWAYS BE ZEROInformationalAcknowledged - 08/21/2023

8. Findings & Tech Details

8.1 SPRINKLERUPGRADEABLE CONTRACT CAN BE DRAINED THROUGH THE EXCHANGETOWATER FUNCTION

// Critical

Description

In the SprinklerUpgradeable contract, a set of assets are whitelisted which can be used to be swapped for WATER. The assets whitelisted can be seen here:

Native Ether, represented by the 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE address, is used to check the multiplier assigned to the Native Ether exchanges:

SprinklerUpgradeable.sol

/**
 * @notice Exchange ETH to water
 * @return waterAmount received water amount
 */
function exchangeETHToWater() external payable nonReentrant returns (uint256 waterAmount) {
    require(msg.value != 0, "Invalid amount");
    waterAmount = getWaterAmount(Constants.ETHER, msg.value);
    if (waterAmount > sprinkleableWater()) revert InsufficientWater();
    require(waterAmount != 0, "No water output"); // if price is 0 or tokenMultiplier is 0, amount can be 0
    transferWater(waterAmount);
    SprinklerStorage.layout().reserves[Constants.ETHER] += msg.value;
    emit WaterExchanged(msg.sender, Constants.ETHER, msg.value, waterAmount, false);
}

SprinklerUpgradeable.sol

function getWaterAmount(
    address _token,
    uint256 _amount
) public view returns (uint256 waterAmount) {
    uint256 multiplier = tokenMultiplier(_token);
    uint256 tokenPrice = IPriceOracleUpgradeable(address(this)).getPrice(_token);
    uint256 waterPrice = IPriceOracleUpgradeable(address(this)).getWaterPrice();
    waterAmount = (_amount * tokenPrice * multiplier) / waterPrice;
}

The function exchangeTokenToWater() is used to exchange whitelisted assets for WATER:

SprinklerUpgradeable.sol

/**
 * @notice Exchange whitelisted asset(BEAN, BEAN:3CRV, Spot, and so on) to water
 * @param token source token address
 * @param amount source token amount
 * @return waterAmount received water amount
 */
function exchangeTokenToWater(
    address token,
    uint256 amount
) external onlyListedAsset(token) nonReentrant returns (uint256 waterAmount) {
    require(token != address(this), "Invalid token");
    require(amount != 0, "Invalid amount");

    waterAmount = getWaterAmount(token, amount);
    if (waterAmount > sprinkleableWater()) revert InsufficientWater();
    require(waterAmount != 0, "No water output"); // if price is 0, amount can be 0

    TransferHelper.safeTransferFrom(token, msg.sender, address(this), amount);
    transferWater(waterAmount);
    SprinklerStorage.layout().reserves[token] += amount;
    emit WaterExchanged(msg.sender, token, amount, waterAmount, false);
}

This function makes use of the TransferHelper library:

TransferHelper.sol

function safeTransferFrom(address token, address from, address to, uint value) internal {
    // bytes4(keccak256(bytes('transferFrom(address,address,uint256)')));
    (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x23b872dd, from, to, value));
    require(success && (data.length == 0 || abi.decode(data, (bool))), 'STF');
}

Although, this library, unlike OpenZeppelin's SafeERC20, does not check that the token address is actually a smart contract with some code deployed in that address (&& address(token).code.length > 0):

SafeERC20.sol

/**
 * @dev Transfer `value` amount of `token` from `from` to `to`, spending the approval given by `from` to the
 * calling contract. If `token` returns no value, non-reverting calls are assumed to be successful.
 */
function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal {
    _callOptionalReturn(token, abi.encodeCall(token.transferFrom, (from, to, value)));
}
function _callOptionalReturnBool(IERC20 token, bytes memory data) private returns (bool) {
    // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since
    // we're implementing it ourselves. We cannot use {Address-functionCall} here since this should return false
    // and not revert is the subcall reverts.

    (bool success, bytes memory returndata) = address(token).call(data);
    return success && (returndata.length == 0 || abi.decode(returndata, (bool))) && address(token).code.length > 0;
}

Based on this and considering that the 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE address is a representative address with no actual code deployed in it, a malicious user could call SprinklerUpgradeable.exchangeTokenToWater("0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", <very high amount>) and drain all the WATER tokens from the contract.

Exploit: poc1.png

Debugged call: poc2.png

BVSS
Recommendation

SOLVED: The Irrigation Protocol team solved the issue by implementing the recommended solution.

Commit ID : 2a75e858618864b3962c623035cca5b5aef4ff4d.

8.2 AUCTIONS AND BIDS COULD BE STUCK PERMANENTLY IN THE AUCTIONUPGRADEABLE CONTRACT IF A BID IS PLACED BY A BLACKLISTED USDC/USDT USER

// Critical

Description

The AuctionUpgradeable contract allows creating different auctions through the createAuction() function:

AuctionUpgradeable.sol

function createAuction(
    uint96 startTime,
    uint96 duration,
    address sellToken,
    uint256 trancheIndex,
    uint128 sellAmount,
    uint128 minBidAmount,
    uint128 fixedPrice,
    uint128 priceRangeStart,
    uint128 priceRangeEnd,
    AuctionType auctionType
) external payable returns (uint256)

Once an auction is completed, the closeAuction() function will be called and will distribute the sellToken between all the bidders. As currently USDC contains a blacklist of users which cannot transfer or receive this token, the following issue could occur:

  1. Alice creates an auction of 1000 USDC(sellToken).
  2. Users place different bids for the USDC auction.
  3. A Blacklisted USDC user also places a bid for the USDC auction. The bid is placed using a whitelisted purchaseToken like DAI.
  4. The auction is completed. closeAuction() is called, but it reverts every time as the USDC transfer to the blacklisted USDC user is forbidden.
  5. The auction USDC sellTokens are now stuck in the contract. All the bids are also stuck permanently in the contract.

  6. Similar issue can occur with USDT as it also contains a blacklist of users.

poc4.png
BVSS
Recommendation

SOLVED: The Irrigation Protocol team solved the issue by enforcing a whitelist on the sellTokens. The whitelist will not include blacklistable tokens like USDC/USDT.

Commit ID : cae1d1718ef504f4fc27ba1aa464600a71ae635a.

8.3 BIDS CAN BE DOS'ED BY PLACING A VERY HIGH BID ON A VERY SMALL BIDAMOUNT

// Medium

Description

As mentioned before, the AuctionUpgradeable contract allows creating different auctions through the createAuction() function:

AuctionUpgradeable.sol

function createAuction(
    uint96 startTime,
    uint96 duration,
    address sellToken,
    uint256 trancheIndex,
    uint128 sellAmount,
    uint128 minBidAmount,
    uint128 fixedPrice,
    uint128 priceRangeStart,
    uint128 priceRangeEnd,
    AuctionType auctionType
) external payable returns (uint256)

The minBidAmount parameter sets the minimum amount of the token that can be bid. In the case that the auctioneer sets the minBidAmount to a very low value, i.e. 1, the following attack vector would be possible:

  1. Alice (auctioneer) creates an auction of 1000 USDC tokens and sets minBidAmount to 1. The auctionType is a TimedAuction.
  2. Bob calls placeBid(1, 1, DAI, 1000e18) bidding 0.01 DAI for 0.000001 USDC. (bidPrice = 1000 DAI).
  3. Joe calls placeBid(1, 100e6, DAI, 1e18) bidding 100 DAI for 100 USDC. (bidPrice = 1 DAI) but it reverts with a low Bid error.
  4. If any user wants to bid now, they should place a bid higher than the abusive bid placed by Bob. (bidPrice = 1000 DAI). Bob managed to cause a Denial of Service, losing only 0.01 DAI.
poc3.png
BVSS
Recommendation

SOLVED: The Irrigation Protocol team solved the issue by implementing the recommended solution.

Commit ID : 0a94a93c0f191c992e9f75a41d630ae3dbf80de8.

8.4 LATESTANSWER CALL MAY RETURN STALE RESULTS

// Medium

Description

In the ChainlinkOracle contract, the function getChainlinkPrice() is used to retrieve prices from different Chainlink aggregators:

ChainlinkOracle.sol

/// @dev returns price with decimals 18
function getChainlinkPrice(AggregatorV2V3Interface feed) internal view returns (uint256) {
    // Chainlink USD-denominated feeds store answers at 8 decimals
    uint256 decimalDelta = uint256(18) - feed.decimals();
    // Ensure that we don't multiply the result by 0
    if (decimalDelta > 0) {
        return uint256(feed.latestAnswer()) * 10 ** decimalDelta;
    } else {
        return uint256(feed.latestAnswer());
    }
}

According to the Chainlink's documentation this function is deprecated and should not be used. Moreover, using latestAnswer() could lead to return invalid/stale prices.

BVSS
Recommendation

SOLVED: The Irrigation Protocol team solved the issue by implementing the recommended solution.

Commit ID : 57d3945089a52425b91cdc8005b62b73d5648c68.

8.5 USER COULD CANCEL THE REST OF BIDS OF AN AUCTION BY DOING 200 DIFFERENT BIDS

// Medium

Description

In the AuctionUpgradeable contract, the internal function _settleAuction() is called every time an auction is closed:

AuctionUpgradeable.sol

function _settleAuction(uint256 auctionId) internal {
    AuctionData memory auction = AuctionStorage.layout().auctions[auctionId];
    uint256 trancheIndex = auction.trancheIndex;
    // when there are no bids, all token amount will be transfered back to seller
    if (auction.curBidId == 0) {
        if (trancheIndex == 0) {
            TransferHelper.safeTransfer(auction.sellToken, auction.seller, auction.reserve);
        } else {
            IERC1155Upgradeable(address(this)).safeTransferFrom(
                address(this),
                auction.seller,
                trancheIndex,
                auction.reserve,
                Constants.EMPTY
            );
        }

        AuctionStorage.layout().auctions[auctionId].reserve = 0;
        AuctionStorage.layout().auctions[auctionId].status = AuctionStatus.Closed;
        emit AuctionClosed(auction.reserve, auctionId);
        return;
    }
    uint128 availableAmount = auction.reserve;
    uint256 settledBidCount = 0;
    uint256 curBidId = auction.curBidId;
    do {
        Bid memory bid = AuctionStorage.layout().bids[auctionId][curBidId];
        uint128 settledAmount = _settleBid(
            auction.sellToken,
            auction.trancheIndex,
            auction.seller,
            bid,
            availableAmount
        );
        availableAmount -= settledAmount;
        AuctionStorage.layout().bids[auctionId][curBidId].bCleared = true;
        --curBidId;
        ++settledBidCount;
    } while (
        curBidId > 0 &&
            settledBidCount <= MAX_CHECK_BID_COUNT &&
            availableAmount >= auction.minBidAmount
    );
    if (availableAmount > 0) {
        if (auction.assetType == AssetType.ERC20) {
            TransferHelper.safeTransfer(auction.sellToken, auction.seller, availableAmount);
        } else {
            IERC1155Upgradeable(address(this)).safeTransferFrom(
                address(this),
                auction.seller,
                trancheIndex,
                availableAmount,
                Constants.EMPTY
            );
        }
    }

    AuctionStorage.layout().auctions[auctionId].reserve = 0;
    AuctionStorage.layout().auctions[auctionId].status = AuctionStatus.Closed;
    emit AuctionClosed(availableAmount, auctionId);
}

This function iterates over all the bids, starting with the last one, transferring the purchased tokens to the bidder until reaching MAX_CHECK_BID_COUNT (which is hardcoded to 200). The rest of the bids will be automatically cancelled.

Based on this, the following attack vector could be possible:

  1. Alice creates an auction:
AuctionUpgradeable(irrigationdiamond).createAuction(
    uint96(block.timestamp), 
    uint96(86400 * 7), 
    address(USDC), 
    0, 
    1000e6, 
    1, 
    1e18, 
    1, 
    2e18, 
    AuctionType.TimedAuction
);
  1. Different users place different bids. In total, 150 bids were placed.
  2. Alice is not happy with the current bid amounts, so she places 200 different bids on her own auction, 1 second before the auction is completed.
  3. The auction is completed. Alice gets her 200 bids, although the rest of the bids of the other users placed before would be automatically canceled.
BVSS
Recommendation

SOLVED: The Irrigation Protocol team solved the issue by implementing the recommended solution.

Commit ID : c269697ae4b7997abec4c836d46e3f73f292af03.

8.6 SWAPETHFORWATER CALL CAN BE SANDWICHED

// Medium

Description

In the WaterTowerUpgradeable contract, the function _swapEthForWater() is used to swap Ether for Beans using the Curve Router:

WaterTowerUpgradeable.sol

function _swapEthForWater(uint256 amount) internal returns (uint256 waterAmount) {
    if (WaterTowerStorage.layout().middleAssetForIrrigate == Constants.BEAN) {
        /// @dev swap ETH for BEAN using curve router
        address[9] memory route = [
            Constants.ETHER,
            Constants.TRI_CRYPTO_POOL,
            Constants.USDT,
            Constants.CURVE_BEAN_METAPOOL,
            Constants.BEAN,
            Constants.ZERO,
            Constants.ZERO,
            Constants.ZERO,
            Constants.ZERO
        ];
        uint256[3][4] memory swapParams = [
            [uint(2), 0, 3],
            [uint(3), 0, 2],
            [uint(0), 0, 0],
            [uint(0), 0, 0]
        ];
        uint256 beanAmount = ICurveSwapRouter(Constants.CURVE_ROUTER).exchange_multiple{
            value: amount
        }(route, swapParams, amount, 0);

        waterAmount = ISprinklerUpgradeable(address(this)).getWaterAmount(
            Constants.BEAN,
            beanAmount
        );
    }
}

This function is calling the exchange_multiple function with no prevention against slippage:

CURVE_ROUTER contract

@external
@payable
def exchange_multiple(
    _route: address[9],
    _swap_params: uint256[3][4],
    _amount: uint256,
    _expected: uint256,
    _pools: address[4]=[ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS],
    _receiver: address=msg.sender
) -> uint256:

Based on this, any autoIrrigate() or irrigate() call could be sandwiched. A malicious user could be monitoring the mempool for any autoIrrigate() or irrigate() call and:

  1. Front-run, the autoIrrigate() or irrigate() call to buy beans from the Curve pool. This pushes the price of Bean in the pool up.
  2. autoIrrigate() or irrigate() call is executed and Beans are bought at a higher price. This raises, once again, the Bean price.
  3. The malicious user back-runs the autoIrrigate() or irrigate() call and sells the Beans at a higher price for a profit.
BVSS
Recommendation

SOLVED: The Irrigation Protocol team solved the issue by implementing the recommended solution.

Commit ID : f573cdc385d11a8ab3b8929e97678ace5221e4d7.

8.7 AUTOIRRIGATECALL WILL ALWAYS REVERT WITH OVERFLOW IF IT'S CALLED WITH THE FULL REWARDAMOUNT

// Low

Description

In the WaterTowerUpgradeable contract, the function autoIrrigate() is used to auto-compound the user rewards:

WaterTowerUpgradeable.sol

function autoIrrigate(address user, uint256 rewardAmount) external onlySuperAdminRole {
    if (!WaterTowerStorage.layout().users[user].isAutoIrrigate) revert NotAutoIrrigate();
    _irrigate(user, rewardAmount);
    /// @dev 870391 is the gasLimit for this function
    uint256 gasFee = 870391 * tx.gasprice;
    WaterTowerStorage.layout().users[user].pending -= gasFee;
    emit AutoIrrigate(user, rewardAmount, gasFee);
}

As this function is called by the protocol admins, the gas fee paid by the admins is subtracted from the user pending balance. Although, the pending balance of the user will always be zero after an _irrigate() call of the full reward amount. Hence, if autoIrrigate() is called with the full reward amount the call will always revert.

BVSS
Recommendation

RISK ACCEPTED: The Irrigation Protocol team accepted this risk, as they state that the autoIrrigate() function will always be called by an admin using full reward amount - fee amount.

8.8 LACK OF A DOUBLE-STEP TRANSFEROWNERSHIP PATTERN

// Informational

Description

The OwnershipFacet contract allows transferring the ownership of the IrrigationDiamond in a single step:

OwnershipFacet.sol

function transferOwnership(address _newOwner) external override {
    LibDiamond.enforceIsContractOwner();
    LibDiamond.setContractOwner(_newOwner);
}

If the nominated EOA account is not a valid account, it is entirely possible that the owner may accidentally transfer ownership to an uncontrolled account, losing the access to all functions with the onlySuperAdminRole modifier.

Score
Recommendation

ACKNOWLEDGED: The Irrigation Protocol team acknowledged this finding.

8.9 CALLING SETMIDDLEASSET WOULD CAUSE THE IRRIGATION BONUS TO ALWAYS BE ZERO

// Informational

Description

The contract WaterTowerUpgradeable contains the function setMiddleAsset():

WaterTowerUpgradeable.sol

function setMiddleAsset(address middleAsset) external onlySuperAdminRole {
    WaterTowerStorage.layout().middleAssetForIrrigate = middleAsset;
}

If this function is ever called to set as middleAssetForIrrigate an asset different from the BEAN token, the irrigation bonus would always be zero as currently the _swapEthForWater() would only work if WaterTowerStorage.layout().middleAssetForIrrigate == Constants.BEAN:

WaterTowerUpgradeable.sol

function _swapEthForWater(uint256 amount) internal returns (uint256 waterAmount) {
    if (WaterTowerStorage.layout().middleAssetForIrrigate == Constants.BEAN) {
        /// @dev swap ETH for BEAN using curve router
        address[9] memory route = [
            Constants.ETHER,
            Constants.TRI_CRYPTO_POOL,
            Constants.USDT,
            Constants.CURVE_BEAN_METAPOOL,
            Constants.BEAN,
            Constants.ZERO,
            Constants.ZERO,
            Constants.ZERO,
            Constants.ZERO
        ];
        uint256[3][4] memory swapParams = [
            [uint(2), 0, 3],
            [uint(3), 0, 2],
            [uint(0), 0, 0],
            [uint(0), 0, 0]
        ];
        uint256 beanAmount = ICurveSwapRouter(Constants.CURVE_ROUTER).exchange_multiple{
            value: amount
        }(route, swapParams, amount, 0);

        waterAmount = ISprinklerUpgradeable(address(this)).getWaterAmount(
            Constants.BEAN,
            beanAmount
        );
    }
}
Score
Recommendation

ACKNOWLEDGED: The Irrigation Protocol team acknowledged this finding.

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.

Slither results

SprinklerUpgradeable.sol

WaterTowerUpgradeable.sol

TrancheBondUpgradeable.sol

AuctionUpgradeable.sol

ERC1155WhitelistUpgradeable.sol

WaterCommonUpgradeable.sol

PriceOracleUpgradeable.sol

PodsOracleUpgradeable.sol

WaterUpgradeable.sol

IrrigationDiamond.sol

Diamond.sol

DiamondCutFacet.sol

DiamondLoupeFacet.sol

EIP2535Initializable.sol

IrrigationAccessControl.sol

OwnershipFacet.sol

PodTransferHelper.sol

LibPrice.sol

BeanPriceOracle.sol

ChainlinkOracle.sol

UniswapV3Twap.sol

  • The unprotected initialize issues flagged by Slither were checked individually and are false positives.

  • No major issues found by Slither.

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 to locate any vulnerabilities.

MythX results

SprinklerUpgradeable.sol

WaterTowerUpgradeable.sol

TrancheBondUpgradeable.sol

AuctionUpgradeable.sol

ERC1155WhitelistUpgradeable.sol

WaterCommonUpgradeable.sol No issues found by MythX.

PriceOracleUpgradeable.sol

PodsOracleUpgradeable.sol

WaterUpgradeable.sol No issues found by MythX.

IrrigationDiamond.sol

Diamond.sol

DiamondCutFacet.sol

DiamondLoupeFacet.sol

EIP2535Initializable.sol

IrrigationAccessControl.sol

OwnershipFacet.sol

PodTransferHelper.sol

LibPrice.sol No issues found by MythX.

BeanPriceOracle.sol No issues found by MythX.

ChainlinkOracle.sol

UniswapV3Twap.sol

  • MythX flagged some integer overflows and underflows which all were false positives, as the contracts are using Solidity ^0.8.17 version. After the Solidity version 0.8.0 Arithmetic operations revert to underflow and overflow by default.

  • MythX also flagged some assert violations, which were all considered to be false positives.

  • No major issues were found by MythX.

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.