Halborn Logo

EVM Smart Contracts - APRO


Prepared by:

Halborn Logo

HALBORN

Last Updated 09/11/2024

Date of Engagement by: July 8th, 2024 - July 26th, 2024

Summary

88% of all REPORTED Findings have been addressed

All findings

8

Critical

0

High

0

Medium

1

Low

1

Informational

6


1. Introduction

APRO engaged Halborn to conduct a security assessment on their smart contracts revisions beginning on 07/08/2024 and ending on 07/26/2024. The security assessment was scoped to the smart contracts provided to the Halborn team.

2. Assessment Summary

The team at Halborn was provided 3 weeks for the engagement and assigned a full-time security engineer to evaluate the security of the smart contract.

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

3. Test Approach and Methodology

Halborn performed a combination of manual and automated security testing to balance efficiency, timeliness, practicality, and accuracy regarding the scope of this assessment. While manual testing is recommended to uncover flaws in logic, process, and implementation; automated testing techniques help enhance code coverage and 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. (Brownie, Anvil, Foundry)


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

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: apro_contract
(b) Assessed Commit ID: c6ab8ce
(c) Items in scope:
  • Owned
  • SimpleWriteAccessController
  • SimpleReadAccessController
↓ Expand ↓
Out-of-Scope:
Remediation Commit ID:
Out-of-Scope: New features/implementations after the remediation commit IDs.

6. Assessment Summary & Findings Overview

Critical

0

High

0

Medium

1

Low

1

Informational

6

Security analysisRisk levelRemediation Date
latestTimestamp create a false sense of data freshnessMediumRisk Accepted
Lack of L2 Sequencer Status Monitoring in DeploymentLowFuture Release
Inconsistent Solidity Pragma VersionsInformationalSolved - 08/06/2024
Unused Function decodeReport in OffchainAggregator ContractInformationalSolved - 08/06/2024
Outdated Version of solidityInformationalAcknowledged
Cache Array Length outside of loopInformationalAcknowledged
Missing SPDX License IdentifierInformationalSolved - 08/06/2024
Inconsistent Naming Convention in Token InterfaceInformationalSolved - 08/06/2024

7. Findings & Tech Details

7.1 latestTimestamp create a false sense of data freshness

// Medium

Description

The current implementation of the latestTimestamp , determineTimestamp and getTimestamp functions in the EACAggregatorCombine contract can mislead users about the freshness of the data if one of the two aggregators has not updated recently.

By returning the latest from the two latest timestamps aggregators, the function may suggest that the data is up-to-date even if one aggregator's data is stale.

function latestTimestamp() public view virtual override returns (uint256) {
    uint256 timestampX = aggregatorX.latestTimestamp();
    uint256 timestampY = aggregatorY.latestTimestamp();
    return timestampX > timestampY ? timestampX : timestampY;
}

Similarly, in the determineTimestamp function, the updatedAt variable is assigned the newer timestamp of the two aggregators:


   function determineTimestamp(
        uint256 startedAtX,
        uint256 startedAtY,
        uint256 updatedAtX,
        uint256 updatedAtY
    ) internal pure returns (uint256 startedAt, uint256 updatedAt) {
        if (/* .. */ )  { /* ... */ } 
        else {
            startedAt = startedAtX > startedAtY ? startedAtY : startedAtX;
            //E @audit updatedAt is the newest of the two
            updatedAt = updatedAtX > updatedAtY ? updatedAtX : updatedAtY;
        }
    }

This issue can create a false sense of data freshness, potentially leading to erroneous decisions based on outdated information. Users might rely on data they believe is recent, but in reality, it could include stale data from one of the aggregators.

Proof of Concept

Test Steps :

  • Aggregator X is updated with data

  • Wait 2 days(can be a lot more)

  • Aggregator Y is updated with data

  • Data and latestTimestamp are retrieved using combine proxy

  • LatestTimestamp of AggregatorY is returned when Aggregator X data is 2 days old


function testLatestTimestampNotLatest() public {
        // Prepare data for the report
        (,,bytes32 configDigest) = offAX.latestConfigDetails();
        uint32 epoch = 1;
        uint8 round = 1;
        bytes32 rawReportContext = bytes32(uint256(uint256(configDigest) << 24 | uint256(epoch) << 8 | uint256(round)));
        bytes32 rawObservers = bytes32(0); // Example value
        int192[] memory observations = new int192[](4);
        observations[0] = 100e18;
        observations[1] = 200e18;
        observations[2] = 300e18;
        observations[3] = 400e18;

        bytes memory report = abi.encode(rawReportContext, rawObservers, observations);

        // Generate signatures for the report
        (bytes32[] memory rs, bytes32[] memory ss, bytes32 rawVs) = generateSignatures(report);

        // Call transmit function
        vm.startPrank(transmitter1);
        offAX.transmit(report, rs, ss, rawVs);
        vm.stopPrank();
        vm.warp(block.timestamp + 2 days);
        vm.startPrank(transmitter2);
        offAY.transmit(report, rs, ss, rawVs);
        vm.stopPrank();

        int256 latestAnswerX = offAX.latestAnswer();
        int256 latestAnswerY = offAY.latestAnswer();

        // console.log("latestAnswerX = %s", uint256(latestAnswerX) / 1e18);
        // console.log("latestAnswerY = %s", uint256(latestAnswerY) / 1e18);
        // console.log("latestAnswerEAC = %s", uint256(eacACP.latestAnswer()) / 1e18);

        console.log("Current Timestamp                        : %s",block.timestamp);
        console.log("Latest Timestamp of EACAggregatorCombine : %s",eacACP.latestTimestamp());
        console.log("Latest Timestamp of Aggregator X         : %s",offAX.latestTimestamp());
        console.log("Latest Timestamp of Aggregator Y         : %s",offAY.latestTimestamp());
    }

As it can be seen in the result, latestTimestamp of AggregatorX is 2 days stale but data is returned like it was updated at the current timestamp

POC1.png
BVSS
Recommendation

To ensure the accuracy and reliability of the reported data, it is recommended to modify the latestTimestamp function to return the oldest timestamp from the two aggregators or to compare it with a maximum staleness variable. This approach ensures that the data is only considered fresh if both aggregators have been updated recently.


Remediation Plan

RISK ACCEPTED : The APRO team accepted the risk, they said that if users want to have an idea of both data freshness they can request both aggregator latest timestamp

References

7.2 Lack of L2 Sequencer Status Monitoring in Deployment

// Low

Description

The audited system, a fork of Chainlink, is designed for deployment across multiple EVM-compatible chains, including Layer 2 (L2) solutions and sidechains. These chains include Scroll, Linea, ZKLink Nova, BSC, and others that are not L2s or L2s. A critical omission has been identified in the implementation: the absence of L2 sequencer feeds contracts on these chains for Apro Contract.

L2 solutions and some sidechains utilize sequencers to batch and process transactions. The operational status of these sequencers is crucial for the integrity and fairness of the oracle service. Without sequencer status monitoring, the system lacks the capability to detect and respond to sequencer downtime or unavailability.

Chainlink's standard implementation includes sequencer feeds to provide real-time status updates of L2 sequencers. These feeds allow oracle consumers to determine whether the L2 network is fully operational and accessible to all users.

The oracle may continue to report prices without accounting for the inaccessibility of the L2 network to most users, potentially leading to mispriced assets or unfair liquidations.

BVSS
Recommendation

To address this issue, one of the following measures is recommended:

  1. Implement L2 Sequencer Feeds: Deploy sequencer status feed contracts on all supported L2 and sidechain networks, following Chainlink's established patterns.

  2. Integrate Sequencer Status Checks: Modify the oracle contracts to incorporate checks against the sequencer status feeds before providing price data.


Remediation Plan

PENDING: The APRO team will solve the issue by deploying a L2 Sequencer Feed on Scroll Rollup

7.3 Inconsistent Solidity Pragma Versions

// Informational

Description

Apro contracts use multiple versions of the Solidity compiler pragma, specifically 0.7.6 and ^0.7.0. This inconsistency can lead to compatibility issues, as different pragma versions may have varying behavior and features. The following files use 0.7.6:

  • AccessControlledOffchainAggregator.sol

  • OffchainAggregator.sol

  • OffchainAggregatorBilling.sol

  • Owned.sol

  • SimpleReadAccessController.sol

  • SimpleWriteAccessController.sol

The following files use ^0.7.0:

  • AccessControllerInterface.sol

  • AggregatorInterface.sol

  • AggregatorV2V3Interface.sol

  • AggregatorV3Interface.sol

  • AggregatorValidatorInterface.sol

  • LinkTokenInterface.sol

  • TypeAndVersionInterface.sol

  • AproTokenInterface.sol

Additionally, the usage of solc-0.7.6 is flagged as not recommended for deployment. It is crucial to use consistent and up-to-date Solidity versions to ensure contract security and compatibility.

Score
Recommendation

It is recommended to use a consistent and up-to-date pragma version across all contracts. It is recommended to upgrade to the latest stable Solidity version (e.g., 0.8.16 or higher) to benefit from the latest security features and improvements.


Remediation Plan

SOLVED : A consistent solidity pragma is now used.

Remediation Hash

7.4 Unused Function decodeReport in OffchainAggregator Contract

// Informational

Description

The function decodeReport in the OffchainAggregator contract is identified as unused and effectively dead code because of the internal modifier. This function is designed to decode a report from a byte array into its constituent components: rawReportContext, rawObservers, and observations. The code is as follows:

function decodeReport(bytes memory _report) internal pure
  returns (
    bytes32 rawReportContext,
    bytes32 rawObservers,
    int192[] memory observations
  )
{
  (rawReportContext, rawObservers, observations) = abi.decode(_report,(bytes32, bytes32, int192[]));
}

The decodeReport function is intended for internal use but is never called within the contract, making it dead code.

Score
Recommendation

It is recommended to remove the decodeReport function from the contract to streamline the codebase and eliminate unnecessary code.


Remediation Plan

SOLVED : The APRO team removed the unused function.

Remediation Hash
References

7.5 Outdated Version of solidity

// Informational

Description

During the security assessment, it was identified that the project uses an outdated version of Solidity (pragma solidity ^0.7.0;). This version was released on July 28, 2020, and has several known bugs that could potentially impact the security and functionality of the smart contracts.

The known bugs in Solidity version 0.7.0 include:

These bugs can lead to unexpected behavior or exposure to known security vulnerabilities.

Score
Recommendation

It is strongly recommended to upgrade to the latest stable version of Solidity to mitigate the risks associated with these known bugs. By upgrading, the project can benefit from the latest security patches, improvements, and new features introduced in the newer versions of Solidity.

Remediation Plan

ACKNOWLEDGED: The APRO team acknowledged the finding.

7.6 Cache Array Length outside of loop

// Informational

Description

When the length of an array is not cached outside of a loop, the Solidity compiler reads the length of the array during each iteration. For storage arrays, this results in an extra sload operation (100 additional gas for each iteration except the first). For memory arrays, this results in an extra mload operation (3 additional gas for each iteration except the first).

For example :

for (uint i = 0; i < _signers.length; i++) { // add new signer/transmitter addresses
      require(
        s_oracles[_signers[i]].role == Role.Unset,
        "repeated signer address"
      );
      s_oracles[_signers[i]] = Oracle(uint8(i), Role.Signer);
      require(s_payees[_transmitters[i]] != address(0), "payee must be set");
      require(
        s_oracles[_transmitters[i]].role == Role.Unset,
        "repeated transmitter address"
      );
      s_oracles[_transmitters[i]] = Oracle(uint8(i), Role.Transmitter);
      s_signers.push(_signers[i]);
      s_transmitters.push(_transmitters[i]);
    }

Score
Recommendation

It is recommended to cache the length of the array in a local variable outside the loop to optimize gas usage. This reduces the number of read operations required during each iteration of the loop.


Remediation Plan

ACKNOWLEDGED: The APRO team acknowledged the finding.

References

7.7 Missing SPDX License Identifier

// Informational

Description

The EACAggregatorProxy.sol does not include an SPDX license identifier on the first line. The SPDX identifier is a recommended standard that specifies the software license governing the code, ensuring clarity and compliance with legal requirements.

pragma solidity 0.7.6;

/**
 * @title The Owned contract
 * @notice A contract with helpers for basic contract ownership.
 */
contract Owned {
    ...
}

The absence of an SPDX license identifier can lead to potential legal ambiguities. Without a clear license declaration, users and developers may be uncertain about the usage, modification, and distribution rights of the contract. This can hinder collaboration and adoption, and may lead to legal complications if the code is used or modified without proper licensing information.

Score
Recommendation

To adhere to best practices and legal standards, it is recommended to include an SPDX license identifier at the beginning of the contract file. This provides clear information about the licensing terms under which the code is distributed.

By adding the SPDX license identifier, you ensure that the licensing information is explicitly communicated, reducing legal risks and promoting better code practices.

Remediation Plan

SOLVED: The APRO team added the SPDX License Identifier to solve the issue.

Remediation Hash
References

7.8 Inconsistent Naming Convention in Token Interface

// Informational

Description

The smart contract under review contains an interface named LinkTokenInterface in file AproTokenInterface.sol which is a direct copy from the Chainlink project. However, this project is named APRO, and the interface name has not been updated to reflect this fact. The current implementation is as follows:

// SPDX-License-Identifier: MIT
pragma solidity ^0.7.0;

interface LinkTokenInterface {
// ... (interface functions)
}

This naming inconsistency is present throughout the interface, including comments that directly reference Chainlink:

// The client constructs and makes a request to a known Chainlink oracle through the transferAndCall function, implemented by the LINK token

Score
Recommendation

To address this issue, the following changes are recommended:

  1. Rename the interface to reflect the APRO project

  2. Update all references to Chainlink in comments and documentation within the interface

Remediation Plan

SOLVED : Naming conventions has been modified.

Remediation Hash
References

8. Automated Testing

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.

Slither1.pngSlither2.png

All issues identified by Slither were proved to be false positives or have been added to the issue list in this report.

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.