Halborn Logo

Protocol - Plural Energy


Prepared by:

Halborn Logo

HALBORN

Last Updated 12/03/2024

Date of Engagement by: October 16th, 2023 - October 27th, 2023

Summary

100% of all REPORTED Findings have been addressed

All findings

14

Critical

0

High

0

Medium

3

Low

5

Informational

6


1. INTRODUCTION

The Plural Protocol is an EVM smart contract protocol for tokenizing RWAs in a regulatory compliant manner. Features include restricted transfer, recovery mechanisms, and continuous dividend accrual and distribution to token holders.

\client engaged Halborn to conduct a security assessment on their smart contracts beginning on 2023-10-16 and ending on 2023-10-27. The security assessment was scoped to the smart contracts provided in the plural-energy/plural-protocol GitHub repository. Commit hashes and further details can be found in the Scope section of this report.

2. ASSESSMENT SUMMARY

Halborn was provided 2 weeks for the engagement and assigned one 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 security risks, that were successfully addressed by Plural Energy. The main ones are the following:

    • Use OpenZeppelin's SafeERC20 wrapper with the IERC20 interface to make the contracts compatible with currencies that return no value.

    • Fix the recover() function of the AssetToken contract to enable the Owner to burn tokens from users.

    • Add a validation to the linkWallet() function that verifies that its child parameter is not a parent.

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

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

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

    • Testnet deployment (Foundry, Brownie).

4. SCOPE

Code repositories:

    1. Plural Protocol

    2. Repository: plural-energy/plural-protocol

    3. Commit ID: dc87e2759f517bdad463f3dbca771ee2a98fb32d

    4. Smart contracts in scope:

      • src/RewardsManagerConstant.sol

      • src/lib/QuarterPeriod.sol

      • src/lib/Structs.sol

      • src/lib/Roles.sol

      • src/lib/MonthPeriod.sol

      • src/AssetFactory.sol

      • src/AssetToken.sol

      • src/BaseAccessControlPausable.sol

      • src/BaseAccessControlPausableUpgradeable.sol

      • src/Compliance.sol

      • src/RewardsManagerRetroactive.sol

      • Fix Commit ID: The fixes of each findings are listed in their corresponding section.

    5. src/RewardsManagerConstant.sol

    6. src/lib/QuarterPeriod.sol

    7. src/lib/Structs.sol

    8. src/lib/Roles.sol

    9. src/lib/MonthPeriod.sol

    10. src/AssetFactory.sol

    11. src/AssetToken.sol

    12. src/BaseAccessControlPausable.sol

    13. src/BaseAccessControlPausableUpgradeable.sol

    14. src/Compliance.sol

    15. src/RewardsManagerRetroactive.sol

    16. Fix Commit ID: The fixes of each findings are listed in their corresponding section.

Out-of-scope

    • Third-party libraries and dependencies.

    • Economic attacks.

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

0

High

0

Medium

3

Low

5

Informational

6

Security analysisRisk levelRemediation Date
USING TRANSFER INSTEAD OF SAFETRANSFERMediumSolved - 11/08/2023
IMPROPER TOKEN RECOVERY IMPLEMENTATIONMediumSolved - 11/21/2023
IMPROPER LINKWALLET VALIDATIONMediumSolved - 11/20/2023
IMPROPER ASSETTOKEN UPDATE FUNCTIONLowSolved - 11/20/2023
IMPROPER REWARDS DISTRIBUTION OVERLAPPING CHECKLowSolved - 11/08/2023
TOKEN ROLES CAN BE ADDED MULTIPLE TIMESLowSolved - 11/08/2023
IMPROPER ROLE-BASED ACCESS CONTROLLowSolved - 11/20/2023
SINGLE STEP OWNERSHIP TRANSFER PROCESSLowSolved - 11/08/2023
OWNER CAN RENOUNCE OWNERSHIPInformationalSolved - 11/21/2023
LACK OF EMERGENCY STOP PATTERN IMPLEMENTATIONInformationalSolved - 11/08/2023
MISSING EVENTS FOR CONTRACT OPERATIONSInformationalSolved - 11/08/2023
LACK OF DISABLEINITIALIZERS IN THE IMPLEMENTATION CONTRACTInformationalSolved - 11/08/2023
FOR LOOPS CAN BE GAS OPTIMIZEDInformationalSolved - 11/08/2023
USING REVERT STRINGS INSTEAD OF CUSTOM ERRORSInformationalSolved - 11/08/2023

8. Findings & Tech Details

8.1 USING TRANSFER INSTEAD OF SAFETRANSFER

// Medium

Description

It was identified that the claim() function in the RewardsManagerRetroactive and RewardsManagerConstant contracts use the IERC20 interface to interact with the rewardTokenAddress to transfer currencies from the contract to the parameter wallet. The IERC20 interface expects the transfer function to have a return value on success. The rewards manager contracts are designed to be used with different currencies. It is important to note that the transfer functions of some tokens (e.g., USDT, BNB) do not return any values, so these tokens are incompatible with the current version of the rewards' manager contracts.

Code Location

The claim() function uses the IERC20 interface to interact with rewardTokenAddress:

src/RewardsManagerRetroactive.sol

    accountClaimedRewards[account] = claimed + unscaledClaimable;
    claimedRewards += unscaledClaimable;

    bool success = IERC20(rewardTokenAddress).transfer(account, claimable);
    if (!success) {
        revert ClaimError();
    }

    emit RewardClaimed(assetTokenAddress, account, unscaledClaimable);

The claim() function reverts if used with tokens not having a return value (e.g., USDT):

halxx_safetransfer.png
BVSS
Recommendation

SOLVED: The \client team solved the issue in commit 3735f2f by using the OpenZeppelin's SafeERC20 wrapper.

8.2 IMPROPER TOKEN RECOVERY IMPLEMENTATION

// Medium

Description

The Owner can recover funds from a compromised account by burning and re-minting tokens. However, it was identified that the recover() function was implemented using the mint() function, which reverts if the total supply is not 0. Therefore, tokens could only be recovered if only one wallet held the entire total supply.

Note that it was also identified that the recover() function does not verify if the wallet is sanctioned.

Code Location

The recover() function uses the AssetToken's mint() function instead of OpenZeppelin's ERC20._mint():

src/AssetToken.sol

function recover(address from, uint256 amount) public override onlyOwner {
    /// @dev: to start, recovery can consist of blocking transfer for a wallet, either through sanctioning
    ///       or role removal, then once that's done we can burn and remint
    /// @dev: require multisig here: e.g. 2/3 multisig with keys held by admin, regulators, and a trusted third party
    address owner = owner();
    _burn(from, amount);
    mint(owner, amount);
    emit Recovered(from, owner, amount);
}

The mint() function reverts if the total supply is not 0:

src/AssetToken.sol

function mint(address to, uint256 amount) public override onlyOwner {
    compliance.authorizeTransfer(msg.sender, to, address(this), amount);
    require(totalSupply() == 0, "Not allowing additional minting");
    require(decimals() <= 18, "Max decimals for token is 18");
    require(amount <= 1e9 * 10 ** decimals(), "Limit tokens to 1 billion to prevent overflow");
    super._mint(to, amount);
    IRewardsManager(rewardsManager).signalAssetTokensMinted();
}

The recover() function reverts:

halxx_recover.png
BVSS
Recommendation

SOLVED: The \client team solved the issue in commits 860295a and bbc76ac by modifying the recover() function to allow the Owner to burn tokens from users.

8.3 IMPROPER LINKWALLET VALIDATION

// Medium

Description

A wallet can be linked to a parent wallet by the Owner with the linkWallet() function of the Compliance contract. The contract assumes that it is not possible for a wallet to be both a child and a parent. Otherwise, there would be cases where the sanction checks could be bypassed. However, because of the improper validation, it is possible to link more than two wallets.

Code Location

Wallets can be linked using the linkWallet() function.

src/Compliance.sol

    function linkWallet(
        address child,
        address parent
    ) external override onlyOwner {
        if (hasRole(Roles.ROLE_SANCTIONED, parent)) {
            revert ParentSanctioned();
        }
        if (linkedWallets[parent] != address(0)) {
            revert ParentWalletIsChild();
        }
        linkedWallets[child] = parent;
        emit LinkedWallet(parent, child);
    }

Reversing the linking order of the wallets bypasses the validation check:

halxx_linkWallet.png
BVSS
Recommendation

SOLVED: The \client team solved the issue in commit a44aa74 by adding the validation.

8.4 IMPROPER ASSETTOKEN UPDATE FUNCTION

// Low

Description

It was identified that the updateTokenImplementation() function in the AssetFactory contract calls the upgradeTo() function of the AssetToken contract, but the upgradeTo() function can only be called by the Owner of the AssetToken contract. Because the Owner is a wallet that manages the AssetToken contract and the AssetFactory does not have the required permission, the updateTokenImplementation() function reverts.

Code Location

src/AssetFactory.sol

    function updateTokenImplementation(
        address token,
        address newImplementation
    ) external override onlyOwner {
        if (newImplementation == address(0)) {
            revert ZeroImplementationAddress();
        }

        AssetToken(token).upgradeTo(newImplementation);
        emit UpdatedTokenImplementation(newImplementation);
    }

src/AssetToken.sol

    function upgradeTo(address newImplementation) public override onlyOwner {
        super.upgradeTo(newImplementation);
    }
BVSS
Recommendation

SOLVED: The \client team solved the issue in commit a44aa74 by configuring the AssetFactory as Owner during initialization and using a different role to manage the AssetToken contract.

8.5 IMPROPER REWARDS DISTRIBUTION OVERLAPPING CHECK

// Low

Description

It was identified that the depositRewards() function in the RewardsManagerConstant contract uses an improper distribution period overlapping validation. Therefore, it is possible to start a new reward period before the end of the current one, despite validation check and documentation. However, it is important to note that the rewards did not stack, as the previous rate and duration is overwritten with the new one, and the new reward is not applied retroactively.

Code Location

The depositRewards() function compares the current block timestamp to the time elapsed since the genesis point, and therefore this validation will always be true:

src/RewardsManagerConstant.sol

    function depositRewards(
        uint64 distributionDuration,
        uint64 rewardPerSecond
    ) external override {
        if (
            !hasRole(rewardsManagerRole, msg.sender) &&
            !hasRole(OWNER_ROLE, msg.sender)
        ) {
            revert NotRewardsManager();
        }
        if (rewardPerSecond > REWARD_PER_SECOND_MAX) {
            revert RewardRateTooHigh();
        }
        /// Prevent underflow with online avgerage calculation, we shouldn't have rewards this small
        /// anyways. At 1e6 precision, this is $0.000001/second => ~$31/year
        if (rewardPerSecond < REWARD_PER_SECOND_MIN) {
            revert RewardRateTooLow();
        }

        uint40 currentTime = uint40(block.timestamp);

        // Check that the now > accrualEnd so we don't stack dividends
        RewardsConfig memory config = _updateReward();
        if (currentTime < config.accrualEnd) {
            revert NoOverlappingRewards();
        }

        if (config.genesisTimestamp <= 0) {
            config.genesisTimestamp = currentTime;
        }

        uint64 distributionEnd = _currentOffset(config.genesisTimestamp) +
            distributionDuration;

        config.receivedRewards += rewardPerSecond * distributionDuration;
        config.accrualEnd = distributionEnd;
        config.currentPeriodRewardPerSecond = rewardPerSecond;

After 1 year, Bob should have earned 3153601 USDT. However, it was possible to start a new reward period that overwrote the original one.

halxx_depositRewards_overwrite.png
BVSS
Recommendation

SOLVED: The \client team solved the issue in commit 09c9c2f by not allowing depositing rewards until the current period ends.

Note that, in this version, it is not possible to cancel or modify the active reward period. However, the administrators can withdraw the deposited reward tokens from the contract.

8.6 TOKEN ROLES CAN BE ADDED MULTIPLE TIMES

// Low

Description

It was identified that the same allowed role can be added to the asset token multiple times. This might result in unexpected behavior, for example, in a deadlock situation where removing the roles would not be possible because of an Index out of range error.

Code Location

Roles can be associated with tokens using the addAllowedRolesToTokens() and resumeRoles() functions:

src/Compliance.sol

function addAllowedRolesToTokens(
    address[] calldata assetTokens,
    string[] calldata roles
) public override onlyOwner {
    for (uint i = 0; i < assetTokens.length; i++) {
        for (uint j = 0; j < roles.length; j++) {
            tokenAllowedRoles[assetTokens[i]].push(
                keccak256(abi.encodePacked(roles[j]))
            );
        }
    }
    emit AddedRolesToTokens(assetTokens, roles);
}

src/Compliance.sol

function resumeRoles(
    address tokenAddress,
    bytes32[] calldata roles
) external override onlyOwner {
    for (uint256 i = 0; i < roles.length; i++) {
        tokenAllowedRoles[tokenAddress].push(roles[i]);
    }
    emit ResumedRoles(tokenAddress, roles);
}

src/Compliance.sol

    function _halt(address tokenAddress, bytes32 roleToRemove) internal {
        bytes32[] memory allowedRoles = tokenAllowedRoles[tokenAddress];
        uint rolesLength = allowedRoles.length;

        for (uint256 x = 0; x < rolesLength; x++) {
            if (allowedRoles[x] == roleToRemove) {
                tokenAllowedRoles[tokenAddress][x] = tokenAllowedRoles[
                    tokenAddress
                ][rolesLength - 1];
                tokenAllowedRoles[tokenAddress].pop();
            }
        }
    }
halxx_add_roles.png
BVSS
Recommendation

SOLVED: The \client team solved the issue in commit 3cae28c.

8.7 IMPROPER ROLE-BASED ACCESS CONTROL

// Low

Description

The Compliance contract has different roles to manage the compliance status of the wallets. However, it was identified that the addRoleToAccount(), addRoleToAccounts() and removeRoleFromAccounts(), sanctionWallets() and sanctionWallet() functions can only be used by the Owner. This limits the ROLE_MANAGER's ability to manage the permissions effectively.

Code Location

The roles are configured in the initialize() function.

src/Compliance.sol

    function initialize(address owner) public override initializer {
        if (owner == address(0)) {
            revert ZeroOwnerAddress();
        }
        __BaseAccessControlPausable__init(owner);

        _setRoleAdmin(Roles.ROLE_MANAGER, OWNER_ROLE);
        _setRoleAdmin(Roles.ROLE_KYC_APPROVED, OWNER_ROLE);
        _setRoleAdmin(Roles.ROLE_US_KYC_APPROVED, OWNER_ROLE);
        _setRoleAdmin(Roles.ROLE_KYC_ACCREDITED, OWNER_ROLE);
        _setRoleAdmin(Roles.ROLE_US_KYC_ACCREDITED, OWNER_ROLE);
        _setRoleAdmin(Roles.ROLE_KYB_APPROVED, OWNER_ROLE);
        _setRoleAdmin(Roles.ROLE_US_KYB_APPROVED, OWNER_ROLE);
        _setRoleAdmin(Roles.ROLE_PRIVATE_PLACEMENT, OWNER_ROLE);
        _setRoleAdmin(Roles.ROLE_PROTOCOL, OWNER_ROLE);
        _setRoleAdmin(Roles.ROLE_PENDING_SANCTIONED, OWNER_ROLE);
        _setRoleAdmin(Roles.ROLE_SANCTIONED, OWNER_ROLE);
    }

However, administrative functions can only be called by the Owner. For example:

src/Compliance.sol

function addRoleToAccounts(
    string calldata roleName,
    address[] calldata accounts
) external override onlyOwner {
    for (uint256 i = 0; i < accounts.length; i++) {
        addRoleToAccount(roleName, accounts[i]);
    }
}
BVSS
Recommendation

SOLVED: The \client team solved the issue in commits df5fa09, f68bb87 and 5232415.

8.8 SINGLE STEP OWNERSHIP TRANSFER PROCESS

// Low

Description

The ownership of the contracts can be lost as the AssetToken contract is inherited from the OwnableUpgradeable contract and its ownership can be transferred in a single-step process. The address the ownership is changed to should be verified to be active or willing to act as the owner.

Code Location

@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol

function transferOwnership(address newOwner) public virtual onlyOwner {
    require(newOwner != address(0), "Ownable: new owner is the zero address");
    _transferOwnership(newOwner);
}

@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol

function _transferOwnership(address newOwner) internal virtual {
    address oldOwner = _owner;
    _owner = newOwner;
    emit OwnershipTransferred(oldOwner, newOwner);
}
BVSS
Recommendation

SOLVED: The \client team solved the issue in commit 971f710 by using the Ownable2StepUpgradeable library over the OwnableUpgradeable library in the AssetToken contract.

8.9 OWNER CAN RENOUNCE OWNERSHIP

// Informational

Description

The AssetToken contract is inherited from the OwnableUpgradeable contract. The Owner of the contract is usually the account that deploys the contract. As a result, the Owner can perform some privileged functions. In the Ownable contracts, the renounceOwnership() function is used to renounce the Owner permission. Renouncing ownership before transferring would result in the contract having no Owner, eliminating the ability to call privileged functions.

Code Location

@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol

function renounceOwnership() public virtual onlyOwner {
    _transferOwnership(address(0));
}
BVSS
Recommendation

SOLVED: The \client team solved the issue in commit f100681 by overriding the renounceOwnership() function.

8.10 LACK OF EMERGENCY STOP PATTERN IMPLEMENTATION

// Informational

Description

It was identified that the whennotpaused modifier is not used in the Compliance contract, even though it is inherited from the BaseAccessControlPausableUpgradeable contract.

BVSS
Recommendation

SOLVED: The \client team solved the issue in commit 514e35d by using the emergency stop pattern with the authorizeTransfer() function.

8.11 MISSING EVENTS FOR CONTRACT OPERATIONS

// Informational

Description

It was identified that the setRewardsManager() function in the AssetToken contract does not emit any events. As a result, it might be more difficult for blockchain monitoring systems to detect suspicious behavior related to these features.

BVSS
Recommendation

SOLVED: The \client team solved the issue in commits 642dcce and 9c42d1e by adding events for all important operations.

8.12 LACK OF DISABLEINITIALIZERS IN THE IMPLEMENTATION CONTRACT

// Informational

Description

The contracts use the Initializable module from OpenZeppelin, and the implementations of these contracts are not marked as initialized in the constructor. An uninitialized instance can be initialized by anyone to take over the contract. Even if it does not affect the instances deployed by the AssetFactory, it is still a good practice to prevent the initialization of the implementation contracts to avoid misuse. In the latest versions, this is done by calling the _disableInitializers function in the constructor.

BVSS
Recommendation

SOLVED: The \client team solved the issue in commit 565ddec by including a constructor to automatically mark the contracts as initialized when they are deployed.

8.13 FOR LOOPS CAN BE GAS OPTIMIZED

// Informational

Description

It was identified that the for loops employed in the Compliance , RewardsManagerConstant and RewardsManagerRetroactive contracts can be gas optimized by the following principles:

  • Unnecessary reading of the array length on each iteration wastes gas.
  • A postfix (e.g. i++) operator was used to increment the i variables. It is known that, in loops, using prefix operators (e.g. ++i) costs less gas per iteration than postfix operators.
  • It is also possible to further optimize loops by using unchecked loop index incrementing and decrementing.

Code Location

Example function using unoptimized for loops:

src/Compliance.sol

function addAllowedRolesToTokens(
    address[] calldata assetTokens,
    string[] calldata roles
) public override onlyOwner {
    for (uint i = 0; i < assetTokens.length; i++) {
        for (uint j = 0; j < roles.length; j++) {
            tokenAllowedRoles[assetTokens[i]].push(
                keccak256(abi.encodePacked(roles[j]))
            );
        }
    }
    emit AddedRolesToTokens(assetTokens, roles);
}
Score
Recommendation

SOLVED: The \client team solved the issue in commit 9a16372 by applying the recommendations.

8.14 USING REVERT STRINGS INSTEAD OF CUSTOM ERRORS

// Informational

Description

It was identified that revert strings are used in the AssetToken, BaseAccessControlPausable and BaseAccessControlPausableUpgradeable contracts. Starting from Solidity v0.8.4, there is a convenient and gas-efficient way to explain to users why an operation failed through the use of custom errors. If the revert string uses strings to provide additional information about failures (e.g. require(amount <= 1e9 * 10 ** decimals(), "Limit tokens to 1 billion to prevent overflow");), but they are rather expensive, especially when it comes to deploying cost, and it is difficult to use dynamic information in them.

Code Location

Example usage of revert strings in the AssetToken contract:

src/AssetToken.sol

function mint(address to, uint256 amount) public override onlyOwner {
    compliance.authorizeTransfer(msg.sender, to, address(this), amount);
    require(totalSupply() == 0, "Not allowing additional minting");
    require(decimals() <= 18, "Max decimals for token is 18");
    require(amount <= 1e9 * 10 ** decimals(), "Limit tokens to 1 billion to prevent overflow");
    super._mint(to, amount);
    IRewardsManager(rewardsManager).signalAssetTokensMinted();
}
Score
Recommendation

SOLVED: The \client team solved the issue in commit d2bbf3a by using custom errors instead of reverting strings.

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. The security team assessed all findings identified by the Slither software, however, findings with severity Information and Optimization are not included in the below results for the sake of report readability.

Results

contracts/AssetFactory.sol \input{slither/AssetFactory}

contracts/AssetToken.sol \input{slither/AssetToken}

contracts/BaseAccessControlPausable.sol Slither did not identify any vulnerabilities in the contract.

contracts/BaseAccessControlPausableUpgradeable.sol Slither did not identify any vulnerabilities in the contract.

contracts/Compliance.sol \input{slither/Compliance}

contracts/RewardsManagerConstant.sol \input{slither/RewardsManagerConstant}

contracts/RewardsManagerRetroactive.sol \input{slither/RewardsManagerRetroactive}

Results Summary

The findings obtained as a result of the Slither scan were reviewed. The majority of Slither findings were determined false-positives.

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.