Halborn Logo

Intent Assets - DappOs


Prepared by:

Halborn Logo

HALBORN

Last Updated 09/05/2024

Date of Engagement by: June 24th, 2024 - July 19th, 2024

Summary

100% of all REPORTED Findings have been addressed

All findings

26

Critical

0

High

1

Medium

3

Low

6

Informational

16


Introduction

DappOS engaged Halborn to conduct a security assessment on their smart contracts beginning on June 26th, 2024, and ending on July 19th, 2024. The security assessment was scoped to the smart contracts provided in the DappOSDao/IntentAssets GitHub repository. Commit hashes and further details can be found in the Scope section of this report.

Assessment Summary

Halborn was provided 3.5 weeks for the engagement, and assigned 1 full-time security engineer to review the security of the smart contracts in scope. The engineer is a blockchain and smart contract security expert with advanced penetration testing and smart contract hacking skills, and deep knowledge of multiple blockchain protocols.

The purpose of the assessment is to:

  • Identify potential security issues within the smart contracts.

  • Ensure that smart contract functionality operates as intended.


In summary, Halborn identified some issues, that were addressed and accepted by the DappOS team. The main security issues were:

  • Missing checks cause DoS & consistent failed redemptions in IntentTokenMinting.

  • Multiple Price Oracle misconfigurations.

Test Approach and 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.

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

  • Testnet deployment (Foundry).

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

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

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

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

2. SCOPE

Files and Repository
(a) Repository: IntentAssets
(b) Assessed Commit ID: 7a6ad39
(c) Items in scope:
  • ./core/token/intentTokenBridge/IntentTokenBridgeStorage.sol
  • ./core/token/intentTokenBridge/IntentTokenBridge.sol
  • ./core/token/intentTokenMinting/IntentTokenMinting.sol
↓ Expand ↓
Out-of-Scope:
Remediation Commit ID:
Out-of-Scope: New features/implementations after the remediation commit IDs.

3. Assessment Summary & Findings Overview

Critical

0

High

1

Medium

3

Low

6

Informational

16

Security analysisRisk levelRemediation Date
Missing checks cause DoS & consistent failed redemptions in IntentTokenMintingHighSolved - 08/19/2024
Chainlink Oracle MisconfigurationMediumSolved - 08/23/2024
Custom sUSDe Oracle MisconfigurationMediumSolved - 08/19/2024
RedStone ETH Oracle MisconfigurationMediumSolved - 08/19/2024
ERC20 approve() Reverts for Non-Standard TokensLowSolved - 08/22/2024
Redemption request bypass through donationsLowRisk Accepted
Initialization can be front-runLowRisk Accepted
Lack of access control can lead to consistent failed plugin transactionsLowSolved - 08/22/2024
Upgradeable contracts are missing storage __gapLowRisk Accepted
Centralization risk for trusted ownersLowSolved - 08/22/2024
Lack of multi-step ownership transferInformationalAcknowledged
Impossible to Pause/Unpause contracts due to unassigned PAUSE_ROLEInformationalAcknowledged
Bricked pools' operations due to unassigned TRANSFER_ROLEInformationalAcknowledged
Impossible to mint IntentToken due to unassigned MINT_ROLEInformationalAcknowledged
Require statement inside loop leads to undesired revertsInformationalAcknowledged
Transfers inside loops can lead to reverted transactionsInformationalAcknowledged
The "nonReentrant" modifier should be placed before other modifiersInformationalAcknowledged
Events fields are missing "indexed" attributeInformationalAcknowledged
Typo in event fieldInformationalAcknowledged
Unused Custom ErrorsInformationalAcknowledged
Use explicit size declarationsInformationalAcknowledged
Modifiers used once can be packed into the functionInformationalAcknowledged
PUSH0 opcode is not supported by all chainsInformationalAcknowledged
Avoid using magic numbersInformationalAcknowledged
Missing checks for address(0)InformationalAcknowledged
Floating pragmaInformationalAcknowledged

4. Findings & Tech Details

4.1 Missing checks cause DoS & consistent failed redemptions in IntentTokenMinting

// High

Description

During the analysis of the IntentTokenMinting.sol contract, it was observed that external calls to the QuotaController.sol contracts are performed in the execution flow of both submit and redeem functions. This happens because before any deposit (through the submit function), or redemption (through redeem function), a quota check must be performed, to ensure that the user's daily quota is not exceeded.

- contracts/core/token/intentTokenMinting/IntentTokenMinting.sol

    function submit(
        address token,
        uint256 tokenAmount
    )
        public
        payable
        whenIsNotCollectingStats
        nonReentrant
        whenNotPaused
        returns (uint256 balanceMinted)
    {
        // 1. check if token is valid
        require(
            intentToken.isUnderlyingAsset(token),
            "IntentTokenMinting: Invalid token"
        );

        // 2. calculate the amount to be minted
        balanceMinted = Calculator.getIntentTokenAmountByUnderlyingAsset(
            token,
            tokenAmount,
            intentToken,
            intentToken.priceOracle()
        );

        require(
            mintQuotaController.checkQuota(
                msg.sender,
                intentToken,
                balanceMinted,
                token,
                tokenAmount
            ),
            "IntentTokenMinting: Quota exceeded"
        );

However, analyzing the checkQuota function itself, it is possible to observe that it lacks access control mechanism, allowing it to be externally called by any entity by passing arbitrary parameters, which are not checked, and will be used in the _deductQuota function, as the execution follows.

- contracts/checker/QuotaController.sol

    function _deductQuota(
        address user,
        address token,
        uint256 tokenAmount
    ) internal {
        TokenInfo storage info = tokenInfo[token];
        UserQuotaInfo storage userInfo = quotaInfo[token][user];
        require(
            info.onceLimit + userInfo.whiteListOnceLimit >= tokenAmount,
            "quotaChecker: Single transaction limit exceeded"
        );
        uint256 today = block.timestamp / 86400;
        if (today != userInfo.lastUpdateDay) {
            userInfo.lastUpdateDay = today;
            userInfo.usage = 0;
        }
        if (today != info.dayUsageLastUpdateDay) {
            info.dayUsageLastUpdateDay = today;
            info.dayUsage = 0;
        }
        require(
            userInfo.usage + tokenAmount <= info.dayLimit + userInfo.whiteListDayLimit,
            "quotaChecker: User daily limit exceeded"
        );
        require(
            info.dayUsage + tokenAmount <= info.dayUsageTotalLimit,
            "quotaChecker: Total daily limit exceeded"
        );
        userInfo.usage += tokenAmount;
        info.dayUsage += tokenAmount;
    }

    function checkQuota(
        address user,
        IIntentToken intentToken,
        uint256 intentTokenAmount,
        address underlyingAsset,
        uint256 underlyingAssetAmount
    )
        external
        override(IMintQuotaController, IBurnQuotaController)
        returns (bool)
    {
        _deductQuota(
            user,
            address(intentToken),
            intentToken.toBaseAssetAmount(intentTokenAmount)
        );
        _deductQuota(user, underlyingAsset, underlyingAssetAmount);
        return true;
    }


This vulnerability allows a malicious user to input any data as a parameters to the checkQuota in the QuotaController.sol contract.

Due to the absence of access control and input validation, the malicious user can trigger _deductQuota on behalf of any user - or all of the users, considering it is possible to fetch through the Submitted event emissions or IntentToken holders.

Consequently, this vulnerability can jeopardize legitimate redemptions, causing the verification process to fail for genuine user requests due to insufficient quota, and ultimately lead to Denial of Service (DoS).

Proof of Concept

To reproduce this vulnerability, the following steps must be considered:

  • A malicious user is aware of all the deposits through the submit function because events are emitted, and therefore, it is possible to know the exact amount all users have deposited. Also, the attacker can know the daily limits established by the setter functions in the contract.

  • With that information, the attacker can craft calls to unprotected checkQuota function in the QuotaController.sol, passing the victims addresses to the address user parameter.

  • This will effectively consume the configured daily quota for any user address passed as parameter. In scale, this can cause generalized Denial of Service, because the malicious actor can call checkQuota without restrictions, in behalf of any user.

  • For simplicity, only one user is affected in this proof of concept.


PoC Code:

    function test_quota_controller_access_control() public {
        /// call function to create state of `submits`
        /// in the IntentTokenMinting contract, so redemptions
        /// can be affected
        test_intent_token_minting_submit();
        vm.startPrank(attacker);
        quota_controller.checkQuota(ghost, intent_token, 47_000 * 1e6, usdt_address, 49_000 * 1e6);
        vm.stopPrank();

        vm.startPrank(ghost);
        IERC20(address(intent_token)).forceApprove(address(intent_token_minting), type(uint256).max - 1);
        IERC20(address(intent_token)).forceApprove(address(cache_pool), type(uint256).max - 1);
        /// calls redeem on IntentTokenMinting which 
        /// will fail, because attacker already consummed quota
        vm.expectRevert();
        intent_token_minting.redeem(950 * 1e18, usdt_address);
        vm.stopPrank();
    }

Stack trace:

    ├─ [0] VM::expectRevert(custom error f4844814:)
    │   └─ ← [Return] 
    ├─ [22643] TransparentUpgradeableProxy::redeem(950000000000000000000 [9.5e20], 0xdAC17F958D2ee523a2206206994597C13D831ec7)
    │   ├─ [22173] IntentTokenMinting::redeem(950000000000000000000 [9.5e20], 0xdAC17F958D2ee523a2206206994597C13D831ec7) [delegatecall]
    │   │   ├─ [856] TransparentUpgradeableProxy::isCollectingStats() [staticcall]
    │   │   │   ├─ [411] IntentToken::isCollectingStats() [delegatecall]
    │   │   │   │   └─ ← [Return] false
    │   │   │   └─ ← [Return] false
    │   │   ├─ [1182] TransparentUpgradeableProxy::isUnderlyingAsset(0xdAC17F958D2ee523a2206206994597C13D831ec7) [staticcall]
    │   │   │   ├─ [734] IntentToken::isUnderlyingAsset(0xdAC17F958D2ee523a2206206994597C13D831ec7) [delegatecall]
    │   │   │   │   └─ ← [Return] true
    │   │   │   └─ ← [Return] true
    │   │   ├─ [917] TransparentUpgradeableProxy::priceOracle() [staticcall]
    │   │   │   ├─ [472] IntentToken::priceOracle() [delegatecall]
    │   │   │   │   └─ ← [Return] TransparentUpgradeableProxy: [0x3eE590DCD55FCD21F8F65F1e208c619188079330]
    │   │   │   └─ ← [Return] TransparentUpgradeableProxy: [0x3eE590DCD55FCD21F8F65F1e208c619188079330]
    │   │   ├─ [865] TransparentUpgradeableProxy::getBaseAsset() [staticcall]
    │   │   │   ├─ [420] IntentToken::getBaseAsset() [delegatecall]
    │   │   │   │   └─ ← [Return] 0xdAC17F958D2ee523a2206206994597C13D831ec7
    │   │   │   └─ ← [Return] 0xdAC17F958D2ee523a2206206994597C13D831ec7
    │   │   ├─ [2372] TransparentUpgradeableProxy::toBaseAssetAmount(950000000000000000000 [9.5e20]) [staticcall]
    │   │   │   ├─ [1924] IntentToken::toBaseAssetAmount(950000000000000000000 [9.5e20]) [delegatecall]
    │   │   │   │   └─ ← [Return] 950000000 [9.5e8]
    │   │   │   └─ ← [Return] 950000000 [9.5e8]
    │   │   ├─ [7173] QuotaController::checkQuota(0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf, TransparentUpgradeableProxy: [0x9858d3c6081Ab26f0bf52238398BffC1F2c4c7a6], 950000000000000000000 [9.5e20], 0xdAC17F958D2ee523a2206206994597C13D831ec7, 950000000 [9.5e8])
    │   │   │   ├─ [2372] TransparentUpgradeableProxy::toBaseAssetAmount(950000000000000000000 [9.5e20]) [staticcall]
    │   │   │   │   ├─ [1924] IntentToken::toBaseAssetAmount(950000000000000000000 [9.5e20]) [delegatecall]
    │   │   │   │   │   └─ ← [Return] 950000000 [9.5e8]
    │   │   │   │   └─ ← [Return] 950000000 [9.5e8]
    │   │   │   └─ ← [Revert] revert: quotaChecker: User daily limit exceeded
    │   │   └─ ← [Revert] revert: quotaChecker: User daily limit exceeded
    │   └─ ← [Revert] revert: quotaChecker: User daily limit exceeded
    ├─ [0] VM::stopPrank()
    │   └─ ← [Return] 
    └─ ← [Stop] 

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 13.80s (5.11s CPU time)

checkQuota(01)
BVSS
Recommendation

It is recommended to apply access control so only the IntentTokenMinting.sol contract can call this function. This can be done by attributing a ROLE to the IntentTokenMinting.sol contract or allow-listing only its address as a legitimate caller to checkQuota in the QuotaController.sol contract.

Remediation

SOLVED: The DappOs team solved the issue as recommended, through the commit hash 3739b73fcbb41d7ccc3b3133b94a37286141e02d.

Remediation Hash

4.2 Chainlink Oracle Misconfiguration

// Medium

Description

Chainlink price feeds exhibit varying update frequencies, also known as 'heartbeats.' For instance, USDC/USD and USDT/USD price feeds have an update frequency of 24 hours (86400 seconds), while ETH/USD updates every hour (3600 seconds).

Also, in the PriceOracle.sol contract initializer, there is no specific configurations that set customizedDelay for a set of assets, which could be done by calling the setCustomizedAcceptableDelay function upon contract's initialization and setting initial configuration for the Price Oracle to operate.

- contracts/core/utils/oracle/PriceOracle.sol

    function initialize(address admin) external initializer {
        _grantRole(DEFAULT_ADMIN_ROLE, admin);
    }

    function _getAssetPriceInBase(
        address tokenAddress
    ) internal view returns (uint256) {
        address feed = chainlinkFeeds[tokenAddress];
        if (feed != address(0)) {
            (, int price, , uint256 updatedAt, ) = AggregatorV3Interface(feed)
                .latestRoundData(); // using Chainlink price feed method
            uint256 _acceptableDelay = customizedDelay[tokenAddress] == 0
                ? acceptableDelay
                : customizedDelay[tokenAddress];
            require(
                updatedAt + _acceptableDelay >= block.timestamp,
                "PriceOracle: update delay exceeds threshold"
            );
            return uint256(price);
        } else {
            ICustomizedOracle customizedOracle = customizedOracles[
                tokenAddress
            ];
            require(
                address(customizedOracle) != address(0),
                "PriceOracle: Unsupported asset"
            );
            return customizedOracle.getPrice();
        }
    }

Neglecting to enforce customizedDelay or using a uniform acceptableDelay for all Chainlink feeds can result in the _getAssetPriceInBase function operating with outdated data or consistently reverting due to mismatched update frequencies.

Furthermore, the current implementation does not include a revert mechanism to handle cases where the Oracle response returns a value of '0' (zero) for price, which can lead to unexpected behavior or errors.


Proof of Concept

To reproduce this vulnerability, consider that no customized delay is enforced for USDC or USDT collateral, and the system is currently using the Chainlink Oracle.

  • Call getAssetPriceInBase in the PriceOracle.sol contract or any function that would subsequently fetch the price using the Chainlink oracle.

  • As no custom delay is enforced for these assets (correct is 86400 seconds), all the transactions depending on this feed information will consistently revert with message PriceOracle: update delay exceeds threshold.


PoC Code:

    function test_chainlink_price_oracle_custom_delay() public {
        /// user `seraph` creates some deposits.
        vm.startPrank(seraph);
        /// usdc
        IERC20(usdc_address).forceApprove(address(intent_token_minting), 5_000 * 1e6);
        vm.expectRevert();
        intent_token_minting.submit(usdc_address, 500 * 1e6);
        /// usdt
        IERC20(usdt_address).forceApprove(address(intent_token_minting), 5_000 * 1e6);
        vm.expectRevert();
        intent_token_minting.submit(usdt_address, 500 * 1e6);

        console.logUint(intent_token.balanceOf(seraph));
        vm.stopPrank();
    }

Stack traces:

    ├─ [73383] TransparentUpgradeableProxy::submit(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48, 500000000 [5e8])
    │   ├─ [68413] IntentTokenMinting::submit(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48, 500000000 [5e8]) [delegatecall]
    │   │   ├─ [7356] TransparentUpgradeableProxy::isCollectingStats() [staticcall]
    │   │   │   ├─ [2411] IntentToken::isCollectingStats() [delegatecall]
    │   │   │   │   └─ ← [Return] false
    │   │   │   └─ ← [Return] false
    │   │   ├─ [3182] TransparentUpgradeableProxy::isUnderlyingAsset(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48) [staticcall]
    │   │   │   ├─ [2734] IntentToken::isUnderlyingAsset(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48) [delegatecall]
    │   │   │   │   └─ ← [Return] true
    │   │   │   └─ ← [Return] true
    │   │   ├─ [2917] TransparentUpgradeableProxy::priceOracle() [staticcall]
    │   │   │   ├─ [2472] IntentToken::priceOracle() [delegatecall]
    │   │   │   │   └─ ← [Return] TransparentUpgradeableProxy: [0x3eE590DCD55FCD21F8F65F1e208c619188079330]
    │   │   │   └─ ← [Return] TransparentUpgradeableProxy: [0x3eE590DCD55FCD21F8F65F1e208c619188079330]
    │   │   ├─ [2865] TransparentUpgradeableProxy::getBaseAsset() [staticcall]
    │   │   │   ├─ [2420] IntentToken::getBaseAsset() [delegatecall]
    │   │   │   │   └─ ← [Return] 0xdAC17F958D2ee523a2206206994597C13D831ec7
    │   │   │   └─ ← [Return] 0xdAC17F958D2ee523a2206206994597C13D831ec7
    │   │   ├─ [3164] 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48::decimals() [staticcall]
    │   │   │   ├─ [2381] 0x43506849D7C04F9138D1A2050bbF3A0c054402dd::decimals() [delegatecall]
    │   │   │   │   └─ ← [Return] 6
    │   │   │   └─ ← [Return] 6
    │   │   ├─ [30824] TransparentUpgradeableProxy::getAssetPriceInBase(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48) [staticcall]
    │   │   │   ├─ [25857] PriceOracle::getAssetPriceInBase(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48) [delegatecall]
    │   │   │   │   ├─ [15643] 0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6::latestRoundData() [staticcall]
    │   │   │   │   │   ├─ [7410] 0x789190466E21a8b78b8027866CBBDc151542A26C::latestRoundData() [staticcall]
    │   │   │   │   │   │   └─ ← [Return] 1715, 99996915 [9.999e7], 1721030447 [1.721e9], 1721030447 [1.721e9], 1715
    │   │   │   │   │   └─ ← [Return] 36893488147419104947 [3.689e19], 99996915 [9.999e7], 1721030447 [1.721e9], 1721030447 [1.721e9], 36893488147419104947 [3.689e19]
    │   │   │   │   └─ ← [Revert] revert: PriceOracle: update delay exceeds threshold
    │   │   │   └─ ← [Revert] revert: PriceOracle: update delay exceeds threshold
    │   │   └─ ← [Revert] revert: PriceOracle: update delay exceeds threshold
    │   └─ ← [Revert] revert: PriceOracle: update delay exceeds threshold

priceOracle(01)
BVSS
Recommendation

It is recommended to initialize proper customizedDelay for all the ChainLink Oracles - in accordance with accepted collaterals, so the checks are consistent with each heart-beat. Additionally, it is recommended to revert the call to the getAssetPriceInBase in case the answer is 0 (zero).

Remediation

SOLVED: The DappOs team solved this issue through the commit hash a45df0114829497e86ffa420ec5b7993b056d207.

Remediation Hash

4.3 Custom sUSDe Oracle Misconfiguration

// Medium

Description

During the assessment of the sUSDeOracle.sol contract, the following areas of improvement were identified:

  1. Unchecked zero answer: The getPrice() function does not validate if the value for usdePriceInUsd is greater than zero.


  2. Price Update Delay Misconfiguration: The state variable PRICE_UPDATE_DELAY is set to 1 hour, incorrectly assuming a heartbeat of 1 hour for sUSDe price feeds. However, the heartbeat for this asset is 1 day (24 hours or 86,400 seconds) in both Chainlink and RedStone oracle networks. To ensure accurate price updates, the PRICE_UPDATE_DELAY should be updated to reflect the correct heartbeat interval.

- contracts/intentUSD/sUSDeOracle.sol

contract sUSDeOracle is ICustomizedOracle {
    uint256 public constant PRICE_UPDATE_DELAY = 1 hours;
    uint8 public immutable USDE_DECIMALS;

    IPriceOracle public immutable parentOracle;
    address public immutable usde;
    IERC4626 public immutable sUSDe;

    constructor(address _parentOracle, address _usde, address _sUSDe) {
        parentOracle = IPriceOracle(_parentOracle);
        usde = _usde;
        sUSDe = IERC4626(_sUSDe);
        USDE_DECIMALS = IERC20Metadata(usde).decimals();
    }

    function decimals() external view override returns (uint8) {
        return USDE_DECIMALS;
    }

    function getPrice() public view virtual override returns (uint256) {
        (uint256 usdePriceInUsd, uint256 usdePriceDecimals) = parentOracle
            .getAssetPriceInBase(usde);
        uint256 usdeAmount = sUSDe.convertToAssets(10 ** sUSDe.decimals());
        require(usdeAmount > 0, "convertToAssets");
        return (usdeAmount * usdePriceInUsd) / (10 ** usdePriceDecimals);
    }
}

Both cases can lead to consistent reverts or undesired values, either because of PriceOracle: update delay exceeds threshold, in case of heart-beat interval misconfiguration or unchecked answer value for usdePriceInUsd, which is obtained by a call to the parentOracle and not verified against 0 (zero).

Proof of Concept

To reproduce this vulnerability, consider that no customized delay is enforced for sUSDe collateral, falling back to the defaulted value in the contract. Call getPrice in the sUSDeOracle.sol contract or any function that would subsequently fetch the price using this oracle.

  • As no custom delay is enforced for these assets (correct is 86400 seconds), all the transactions depending on this feed information will consistently revert with messagePriceOracle: update delay exceeds threshold.

PoC Code:

    function test_susde_oracle_get_price() public {
        vm.startPrank(attacker);
        uint256 susde_price_custom_oracle = susde_oracle.getPrice();
        console.logString("sUSDe Reported price in Custom Oracle (sUSDE.sol) is:");
        console.logUint(susde_price_custom_oracle);
        vm.stopPrank();
    }

Stack traces:

    ├─ [33885] sUSDeOracle::getPrice() [staticcall]
    │   ├─ [30916] TransparentUpgradeableProxy::getAssetPriceInBase(0x4c9EDD5852cd905f086C759E8383e09bff1E68B3) [staticcall]
    │   │   ├─ [25949] PriceOracle::getAssetPriceInBase(0x4c9EDD5852cd905f086C759E8383e09bff1E68B3) [delegatecall]
    │   │   │   ├─ [15735] 0xa569d910839Ae8865Da8F8e70FfFb0cBA869F961::latestRoundData() [staticcall]
    │   │   │   │   ├─ [7502] 0xB735cC58d71dEAc4cfC46dE68d3b04988F7D7b2d::latestRoundData() [staticcall]
    │   │   │   │   │   └─ ← [Return] 88, 99895379 [9.989e7], 1721339447 [1.721e9], 1721339447 [1.721e9], 88
    │   │   │   │   └─ ← [Return] 18446744073709551704 [1.844e19], 99895379 [9.989e7], 1721339447 [1.721e9], 1721339447 [1.721e9], 18446744073709551704 [1.844e19]
    │   │   │   └─ ← [Revert] revert: PriceOracle: update delay exceeds threshold
    │   │   └─ ← [Revert] revert: PriceOracle: update delay exceeds threshold
    │   └─ ← [Revert] revert: PriceOracle: update delay exceeds threshold
    └─ ← [Revert] revert: PriceOracle: update delay exceeds threshold

sUSDeOracle(01)
BVSS
Recommendation

It is recommended to set proper delay, accordingly to the heart-beat of the targeted feed. Additionally, it is recommended to revert the call in case the oracle's answer is 0 (zero).

Remediation

SOLVED: The DappOs team solved this issue through the commit hashes 22b22ada6cbc33a1f7abee1d17107d4d99362de1.

Remediation Hash

4.4 RedStone ETH Oracle Misconfiguration

// Medium

Description

The RedStoneInETHOracle.sol contract is designed for ETH pairs, which have different heart-beats than the hard-coded 6-hour PRICE_UPDATE_DELAY value in the contract under analysis. According to the RedStone Price Feeds documentation, most ETH pairs in RedStone, such as ETH-WETH, operate with a 24-hour heartbeat.

Using a hard-coded value for PRICE_UPDATE_DELAY can lead to reverts due to "Data is outdated" in price feeds with longer heart-beats, such as 24 hours. To prevent this issue, it is recommended to use a dynamic value that accurately reflects the heart-beat of the specific ETH pair being used.

- contracts/intentETH/RedStoneInETHOracle.sol

    function getPrice() external view override returns (uint256 price) {
        uint256 latestUpdateTime = IRedstoneAdapter(feed)
            .getBlockTimestampFromLatestUpdate();
        require(
            latestUpdateTime + PRICE_UPDATE_DELAY >= block.timestamp,
            "Data is outdated"
        );
        uint256 basePrice = IPriceCalculator(feed).priceOfETH();
        uint256 tokenPrice = token == address(0)
            ? basePrice
            : IPriceCalculator(feed).priceOf(token);
        return (tokenPrice * 10 ** 18) / basePrice;
    }

Additionally, the affected contract does not validate the answer values for basePrice and tokenPrice against zero, which should be enforced to prevent undesired and unpredicted behavior.

Proof of Concept

To reproduce this vulnerability, call getPrice in the RedStoneInETHOracle.sol contract. By default, the call should revert because of heart-beat misconfiguration, as follows:

PoC Code:

    function test_redstone_eth_get_price() public {
        vm.startPrank(ghost);
        redstone_eth_oracle.getPrice();
        vm.stopPrank();
    }

Stack traces:

    ├─ [12848] RedStoneInETHOracle::getPrice() [staticcall]
    │   ├─ [9604] 0x8751F736E94F6CD167e8C5B97E245680FbD9CC36::getBlockTimestampFromLatestUpdate() [staticcall]
    │   │   ├─ [2447] 0x031A4f6342175c6B1207c98575258be3B55407EB::getBlockTimestampFromLatestUpdate() [delegatecall]
    │   │   │   └─ ← [Return] 1721289827 [1.721e9]
    │   │   └─ ← [Return] 1721289827 [1.721e9]
    │   └─ ← [Revert] revert: Data is outdated
    └─ ← [Revert] revert: Data is outdated

RedStoneInETHOracle(01)
BVSS
Recommendation

Enforce the correct price update delay based on each asset heat-beat on corresponding price feed in RedStone ecosystem. In addition, it is recommended to ensure that the values for basePrice and tokenPrice (answers) are not zero (0).

The official documentation should be used as a reference.

Remediation

SOLVED: The DappOs team solved this issue through the commit hashes c5f1af0b8aa1f131c9d0fc104e25b515de87cdb5 and 728fd1c04228b911fda135aee98f1e925704fcff.

Remediation Hash

4.5 ERC20 approve() Reverts for Non-Standard Tokens

// Low

Description

The current implementation of the sDaiMintingPlugin.sol contract utilizes the approve method to set allowances for ERC20 tokens.

However, this approach may cause a revert if the target ERC20 token employs a non-standard implementation with a different function signature for the approve() function.

- contracts/intentUSD/sDaiMintingPlugin.sol

    function execute(
        address tokenToReceive,
        address inputToken,
        uint256 inputAmount,
        bytes memory
    ) external override {
        address dai = ISavingsDai(sDAI).asset();
        require(dai == inputToken, "SDaiMintingPlugin: Invalid input token");
        IERC20(inputToken).approve(sDAI, inputAmount);
        ISavingsDai(sDAI).deposit(inputAmount, tokenToReceive);
    }

In the example above, if inputToken is a non-standard ERC-20 implementation, the transaction will revert consistently because of mis-handle of the approve function.

BVSS
Recommendation

To ensure compatibility with all ERC20 tokens, including those with non-standard implementations, it is recommended to use SafeERC20's forceApprove method instead. This will provide a more robust solution for managing token allowances across various ERC20 token contracts.


Remediation

SOLVED: The DappOs team solved this issue through the commit hash c6b4b09904ea8d8a542cf6e8604ad090636aabde.

Remediation Hash

4.6 Redemption request bypass through donations

// Low

Description

The redeem function's behavior in the CachePool.sol contract can be manipulated with donation attacks. The function relies on the _getTokenBalance helper function to check the contract's balance for the underlying asset. If the balance is equal to or greater than the requested underlyingAssetAmount, the _redeem function is called, and the redemption is processed immediately (returning 0).

- contracts/core/pools/cachePool/CachePool.sol

    function redeem(
        address user,
        uint256 intentTokenAmount,
        address underlyingAsset,
        uint256 underlyingAssetAmount
    )
        external
        nonReentrant
        onlyIntentTokenMinting
        whenNotPaused
        returns (uint256)
    {
        TransferHelper.safeTransferFrom(
            address(intentToken),
            user,
            address(this),
            intentTokenAmount
        );
        if (Common._getTokenBalance(underlyingAsset) >= underlyingAssetAmount) {
            _redeem(
                user,
                underlyingAsset,
                underlyingAssetAmount,
                intentTokenAmount
            );

            // We return 0 to indicate a complete redemption
            return 0;
        } else {
            RequestData memory _requestData = RequestData(
                underlyingAsset,
                underlyingAssetAmount,
                intentToken,
                intentTokenAmount,
                block.timestamp + claimExpiryPeriod,
                RequestStatus.CREATED
            );
            requests[user][++requestId] = _requestData;
            emit RequestSent(
                user,
                _requestData.underlyingAsset,
                _requestData.intentToken,
                requestId
            );

            return requestId;
        }
    }

However, if the contract's balance is lower than the requested amount, a redemption request is created, and the function returns a requestId.

In this scenario, a malicious actor could donate the required ERC-20 underlying asset (or native ETH) directly to the CachePool contract. This donation would bypass the redemption request mechanism, as described in the specification document.

The specification states that if the CachePool has sufficient funds and meets certain rules, users can immediately obtain the underlying assets. Otherwise, a request is created, and users must wait for the administrator to deposit funds into the CachePool before they can claim the underlying assets.

The donation of underlyingAsset allows the malicious actor to bypass this process. Since there is no apparent financial incentive for the attacker, the severity of this vulnerability is low.

BVSS
Recommendation

Use internal methods (like mappings) for tracking balances instead of balanceOf and/or balance.

Remediation

RISK ACCEPTED: The DappOs team accepted the risk related to this finding.

4.7 Initialization can be front-run

// Low

Description

In the PriceOracle.sol contract, it was identified an unprotected initializer, which is susceptible to front-running attacks.

- contracts/core/utils/oracle/PriceOracle.sol

    function initialize(address admin) external initializer {
        _grantRole(DEFAULT_ADMIN_ROLE, admin);
    }
BVSS
Recommendation

Use _disableInitializers() in the constructor in order to prevent initialization front-running.

Remediation

RISK ACCEPTED: The DappOs team accepted the risk related to this finding.

4.8 Lack of access control can lead to consistent failed plugin transactions

// Low

Description

The checkWear function in the WearChecker.sol contract lacks proper access control, allowing any low-privileged user to call the function and manipulate the dayUsage state variable. This vulnerability enables an attacker to write malicious state data, effectively disrupting all transactions processed through plugins in the CachePool.sol contract.

- contracts/checker/WearChecker.sol

    function checkWear(
        address tokenToReceive,
        address tokenToUse,
        uint256 receivedAmount,
        uint256 usedAmount
    ) external returns (bool) {
        uint256 receivedValueByIntentToken = Calculator
            .getIntentTokenAmountByUnderlyingAsset(
                tokenToReceive,
                receivedAmount,
                intentToken,
                intentToken.priceOracle()
            );
        uint256 usedValueByIntentToken = Calculator
            .getIntentTokenAmountByUnderlyingAsset(
                tokenToUse,
                usedAmount,
                intentToken,
                intentToken.priceOracle()
            );

        uint256 wearValueByIntentToken = receivedValueByIntentToken >=
            usedValueByIntentToken
            ? 0
            : usedValueByIntentToken - receivedValueByIntentToken;

        if (
            wearValueByIntentToken * maxSingleTransactionWearRate >
            usedValueByIntentToken
        ) {
            return false;
        }
        uint256 wearValue = intentToken.toBaseAssetAmount(
            wearValueByIntentToken
        );

        uint256 today = block.timestamp / 86400;
        if (today != dayUsageLastUpdateDay) {
            dayUsageLastUpdateDay = today;
            dayUsage = 0;
        }

        if (wearValue + dayUsage > dayUsageTotalLimit) {
            return false;
        }
        dayUsage += wearValue;
        return true;
    }

By sending malicious calls to the checkWear function, an attacker can force the daily limit to be reached. Consequently, the checkWear function will return false, halting the execution of the processPluginTransaction function in the CachePool.sol contract.

- contracts/core/pools/cachePool/CachePool.sol

    function processPluginTransaction(
        address tokenToReceive,
        address tokenToUse,
        uint256 usedAmount,
        IExecutionPlugin pluginContract,
        bytes memory data
    ) external onlyRole(TRANSFER_ROLE) nonReentrant whenNotPaused {
        require(plugins[pluginContract], "CachePool: Unauthorized plugin");
        uint256 balanceBefore = Common._getTokenBalance(tokenToReceive);

        TransferHelper.safeTransfer2(
            tokenToUse,
            address(pluginContract),
            usedAmount
        );
        pluginContract.execute(tokenToReceive, tokenToUse, usedAmount, data);

        uint256 receivedAmount = Common._getTokenBalance(tokenToReceive) -
            balanceBefore;
        require(
            wearChecker.checkWear(
                tokenToReceive,
                tokenToUse,
                receivedAmount,
                usedAmount
            ),
            "CachePool: Excessive wear"
        );
        emit PluginExecuted(
            tokenToReceive,
            tokenToUse,
            usedAmount,
            receivedAmount,
            pluginContract
        );
    }

This disruption can lead to denial of service, as all transactions processed through processPluginTransaction will consistently revert, because the require statement won't be met, considering checkWear will return false after the limit is manipulated by the attacker.

BVSS
Recommendation

It is recommended to add proper access control to the checkWear function in the WearChecker.sol contract, in order to prevent low-privileged users to call this function, preventing potential deviations from the original intended behavior.

Remediation

SOLVED: The DappOs team solved this issue through the commit hash d33618340d547ed9f61baa3e838483e9432108b8.

Remediation Hash

4.9 Upgradeable contracts are missing storage __gap

// Low

Description

In-scope there are upgradeable contracts, and therefore, it is recommended to use the __gap state variable to ensure that the state of the contract will be preserved across upgrades.

In any of the upgradeable contracts, neither on their Storage contract, was identified the uint256[50] private __gap variable.

As by convention, and to preserve the functionalities of the gap variable, which are, keeping consistency of the contract's storage layout across updates, it must be inserted at the end of the contract, or of the Storage contract of each upgradeable contract.

BVSS
Recommendation

Place the uint256[50] private __gap; variable at the end of upgradeable contracts or their Storage contract.

Remediation

RISK ACCEPTED: The DappOs team accepted the risk related to this finding.

4.10 Centralization risk for trusted owners

// Low

Description

The contracts in-scope are safe-guarded by an access control mechanism, which is a good practice in smart contract development and prevents non-authorized parties and bad actors to perform unauthorized activities within the contracts and ecosystem.

Owners with privileged rights to perform administrative operations need to be trusted and have power mitigated to prevent unilateral actions.

BVSS
Recommendation

It is recommended to enforce a Multi-signature mechanism, in order to mitigate potential centralization concerns regarding administrative tasks.

Remediation

SOLVED: The DappOs team informed that is mitigating centralization risk through a multi-signature wallet.

4.11 Lack of multi-step ownership transfer

// Informational

Description

To enhance the security and flexibility of ownership transfer in smart contracts, it is recommended to use a two-step ownership transfer process. This approach allows for a more controlled transfer of ownership, reducing the risk of unintended consequences or lost access to contract functionalities.

OpenZeppelin provides a convenient implementation of this pattern in the form of the Ownable2Step contract. By using Ownable2Step, you can separate the process of ownership transfer into two distinct steps: proposing a new owner and accepting the proposed ownership.

BVSS
Recommendation

Implement Ownable2Step instead of Ownable, for enhanced security when transferring contract's ownership.

Remediation

ACKNOWLEDGED: The DappOs team acknowledged this issue.

4.12 Impossible to Pause/Unpause contracts due to unassigned PAUSE_ROLE

// Informational

Description

Contracts MainPool.sol, CachePool.sol, IntentTokenMinting.sol and IntentTokenBridge.sol utilize OpenZeppelin's Role-Based Access Control (RBAC) to manage function permissions. One of the roles in these contracts is the PAUSE_ROLE, which is responsible for controlling the ability to pause and unpause the contracts. However, during contract initialization, the PAUSE_ROLE is not assigned to any entity, be it a contract or a wallet.

BVSS
Recommendation

It is recommended to grant a PAUSE_ROLE to at least one entity, that will be capable of pausing and unpausing the contract since the initialization.

Remediation

ACKNOWLEDGED: The DappOs team acknowledged this issue.

4.13 Bricked pools' operations due to unassigned TRANSFER_ROLE

// Informational

Description

The MainPool.sol and CachePool.sol contracts leverage OpenZeppelin's (OZ) Role-Based Access Control (RBAC) for managing function permissions.

During both contracts' initialization, the DEFAULT_ADMIN_ROLE is assigned, but the TRANSFER_ROLE is not attributed to any entity, be it a contract or a wallet. As a consequence, functions that require the caller to have the TRANSFER_ROLE will consistently revert, rendering them inoperable.

- contracts/core/pools/cachePool/CachePool.sol

    function initialize(address admin) external initializer {
        __AccessControl_init();
        __ReentrancyGuard_init();
        _grantRole(DEFAULT_ADMIN_ROLE, admin);
    }

- contracts/core/pools/mainPool/MainPool.sol

    function initialize(
        address admin,
        IIntentToken _intentToken
    ) external initializer {
        intentToken = _intentToken;
        emit IntentTokenSet((address(_intentToken)));
        __AccessControl_init();
        __Pausable_init();
        __ReentrancyGuard_init();
        _grantRole(DEFAULT_ADMIN_ROLE, admin);
    }

Specifically, functions transferToMainPool, processPluginTransaction and callWhitelisted in the CachePool.sol, and callWhitelisted and transferToWhitelist in the MainPool.sol are affected by this issue.

BVSS
Recommendation

It is recommended to assign the TRANSFER_ROLE to the appropriate entities during contract initialization. This will enable authorized parties to execute the affected functions as intended.

Remediation

ACKNOWLEDGED: The DappOs team acknowledged this issue.

4.14 Impossible to mint IntentToken due to unassigned MINT_ROLE

// Informational

Description

The IntentToken.sol contract employs OpenZeppelin's Role-Based Access Control (RBAC) to manage function permissions. Specifically, the mint function requires the caller to possess the MINT_ROLE. During contract initialization, the DEFAULT_ADMIN_ROLE is assigned through the initialize function.

    function initialize(
        address admin,
        string memory name,
        string memory symbol,
        address baseAsset_
    ) external initializer {
        __AccessControl_init();
        __Pausable_init();
        __IntentToken_init(name, symbol, baseAsset_);

        _grantRole(DEFAULT_ADMIN_ROLE, admin);
    }

However, the contract does not attribute the MINT_ROLE to any party or entity. As a consequence, the mint function becomes impossible to execute, as there is no match for the required role, resulting in no allowed callers to the function.

BVSS
Recommendation

To resolve this issue, it is essential to assign the MINT_ROLE to at least one address during the contract's initialization. This will ensure that the mint function can be executed by authorized parties, enabling the proper functioning of the contract.

Remediation

ACKNOWLEDGED: The DappOs team acknowledged this issue.

4.15 Require statement inside loop leads to undesired reverts

// Informational

Description

Using require / revert statements within a loop can pose a risk to transaction processing, as a single problematic item can cause the entire transaction to fail. This behavior is noticed in the callWhitelisted function of the MainPool.sol contract, as follows:

- contracts/core/pools/mainPool/MainPool.sol

    function callWhitelisted(
        address to,
        ICalldataValidator calldataValidator,
        bytes calldata data
    )
        public
        payable
        override(WhitelistedCaller)
        onlyRole(TRANSFER_ROLE)
        nonReentrant
        whenNotPaused
        returns (bool success, bytes memory returnData)
    {
        (success, returnData) = super.callWhitelisted(
            to,
            calldataValidator,
            data
        );
        require(success, "MainPool: Call failed");
        uint256 underlyingAssetsLength = underlyingAssets.length();
        for (uint256 i = 0; i < underlyingAssetsLength; ++i) {
            require(
                Common._getTokenBalance(underlyingAssets.at(i)) >=
                    totalDeposited[underlyingAssets.at(i)],
                "MainPool: Insufficient balance"
            );
        }
    }

While iterating over the underlyingAssets array in the given function, a potential issue arises when using require statements within the loop. If the asset at position i does not satisfy the require statement, the entire transaction will fail, even if the subsequent asset at position i+1 would have satisfied the requirement.

BVSS
Recommendation

Consider handling cases where the requirement is not met more gracefully, using try/catch or exclude them from the iteration by creating an array of only valid assets, which balance would satisfy the requirement.

Remediation

ACKNOWLEDGED: The DappOs team acknowledged this issue.

4.16 Transfers inside loops can lead to reverted transactions

// Informational

Description

In the transferToMainPool function of the CachePool.sol contract, multiple transfers are executed inside a loop. This implementation can lead to reverted transactions if any individual transfer fails, causing the entire operation to revert.

The function accepts two arrays, tokens and amounts, and iterates over them to perform transfers using the safeTransfer2 function from TransferHelper. If any transfer fails due to insufficient balance, invalid token contracts, or other unforeseen issues, the entire transaction will be reverted.

- contracts/core/pools/cachePool/CachePool.sol

    function transferToMainPool(
        address[] memory tokens,
        uint256[] memory amounts
    ) external onlyRole(TRANSFER_ROLE) whenNotPaused nonReentrant {
        require(
            tokens.length == amounts.length,
            "CachePool: Mismatched array lengths"
        );
        for (uint256 i = 0; i < tokens.length; ++i) {
            TransferHelper.safeTransfer2(
                tokens[i],
                address(intentToken.mainPool()),
                amounts[i]
            );
            emit TransferToMainPool(tokens[i], amounts[i]);
        }
    }
BVSS
Recommendation

Consider excluding the tokens without sufficient balance from the iteration, in order to prevent undesired reverts.

Remediation

ACKNOWLEDGED: The DappOs team acknowledged this issue.

4.17 The "nonReentrant" modifier should be placed before other modifiers

// Informational

Description

In order to avoid reentrancy in other modifiers, the nonReentrant modifier should be placed before all other modifiers.

The behavior was identified in the following instances:

- contracts/core/pools/cachePool/CachePool.sol

	    ) external onlyRole(TRANSFER_ROLE) whenNotPaused nonReentrant {

- contracts/core/pools/mainPool/MainPool.sol

	    ) external payable whenNotPaused whenIsNotCollectingStats nonReentrant {

Score
Impact:
Likelihood:
Recommendation

Shift the nonReentrant modifier so it is the first modifier on the function declaration.


Remediation

ACKNOWLEDGED: The DappOs team acknowledged this issue.

4.18 Events fields are missing "indexed" attribute

// Informational

Description

Indexed event fields make the data more quickly accessible to off-chain tools that parse events, and add them to a special data structure known as “topics” instead of the data part of the log. A topic can only hold a single word (32 bytes) so if you use a reference type for an indexed argument, the Keccak-256 hash of the value is stored as a topic instead.

Each event can use up to three indexed fields. If there are fewer than three fields, all the fields can be indexed. It is important to note that each index field costs extra gas during emission, so it's not necessarily best to index the maximum allowed fields per event (three indexed fields).

This is specially recommended when gas usage is not particularly of concern for the emission of the events in question, and the benefits of querying those fields in an easier and straight-forward manner surpasses the downsides of gas usage increase.

During the analysis of the contracts in-scope, it was identified multiple (22) instances of event fields that could have indexed attribute.

Score
Impact:
Likelihood:
Recommendation

It is recommended to add the indexed keyword when declaring events, considering the following example:

    event Indexed(
        address indexed from,
        bytes32 indexed id,
        uint indexed value
    );

Remediation

ACKNOWLEDGED: The DappOs team acknowledged this issue.

4.19 Typo in event field

// Informational

Description

During the analysis of the BaseCalldataValidator.sol contract, it was identified a possible typo in the oladEthAmount, which is believed to be oldEthAmount correct in this case.

- contracts/checker/common/BaseCalldataValidator.sol

    event MaxEthAmountChanged(
        uint256 indexed newEthAmount,
        uint256 indexed oladEthAmount
    );
Score
Impact:
Likelihood:
Recommendation

Make sure to avoid typos in declarations for enhanced code readability.


Remediation

ACKNOWLEDGED: The DappOs team acknowledged this issue.

4.20 Unused Custom Errors

// Informational

Description

During the analysis of the smart contracts in-scope, it was identified that some Custom Errors are not being used. The following instances were detected:

- contracts/core/utils/crossChainMessageRelayer/LzSenderReceiver.sol

error NotEnoughNative(uint256 msgValue);

- contracts/core/utils/crossChainMessageRelayer/LzSenderReceiver.sol

error LzTokenUnavailable();

- contracts/core/utils/crossChainMessageRelayer/OptionsBuilder.sol

error InvalidSize(uint256 max, uint256 actual);


Score
Impact:
Likelihood:
Recommendation

Unused custom errors should have their definition removed.


Remediation

ACKNOWLEDGED: The DappOs team acknowledged this issue.

4.21 Use explicit size declarations

// Informational

Description

Using explicit size declarations helps avoid confusion and ensures that developers are aware of the exact variable size they are working with. This practice promotes better code quality, reduces the potential for errors, and enhances the overall maintainability of the smart contract.

- contracts/checker/common/ToValidator.sol

for (uint i = 0; i < length; i++) {

- contracts/core/libraries/TransferHelper.sol

uint value

Score
Impact:
Likelihood:
Recommendation

To maintain consistency and improve code readability within a contract, opt for uint256 and int256 instead of the implicit uint and int declarations.


Remediation

ACKNOWLEDGED: The DappOs team acknowledged this issue.

4.22 Modifiers used once can be packed into the function

// Informational

Description

If a modifier is invoked only a single time within a contract, it may be more efficient and cleaner to integrate the modifier's logic directly into the function's body.


- contracts/core/pools/cachePool/CachePool.sol

modifier onlyIntentTokenMinting() {

- contracts/core/pools/mainPool/MainPool.sol

modifier onlyIntentToken() {

Score
Impact:
Likelihood:
Recommendation

Consider incorporating the modifier logic inside the function that is using it.


Remediation

ACKNOWLEDGED: The DappOs team acknowledged this issue.

4.23 PUSH0 opcode is not supported by all chains

// Informational

Description

Solc compiler version 0.8.20 switches the default target EVM version to Shanghai. The generated bytecode will include PUSH0 opcodes. The recently released Solc compiler version 0.8.25 switches the default target EVM version to Cancun, so it is also important to note that it also adds-up new opcodes such as TSTORE, TLOAD and MCOPY.

Be sure to select the appropriate EVM version in case you intend to deploy on a chain apart from Ethereum mainnet, like L2 chains that may not support PUSH0, TSTORE, TLOAD and/or MCOPY, otherwise deployment of your contracts will fail.

Score
Impact:
Likelihood:
Recommendation

It is important to consider the targeted deployment chains before writing immutable contracts because, in the future, there might exist a need for deploying the contracts in a network that could not support new opcodes from Shanghai or Cancun EVM versions.


Remediation

ACKNOWLEDGED: The DappOs team acknowledged this issue.

4.24 Avoid using magic numbers

// Informational

Description

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

By using a constant state variable, you can enhance code clarity, reduce the potential for errors, and facilitate easier updates. If the value needs to be changed, you can modify it in a single location, rather than searching for and updating each instance of the magic number. This practice promotes better code quality and simplifies the maintenance process.

- contracts/core/libraries/AddressString.sol

stringBytes.length != 42 ||

- contracts/core/libraries/AddressString.sol

for (uint256 i = 2; i < 42; ++i) {

- contracts/core/libraries/AddressString.sol

else if ((stringByte >= 48) && (stringByte <= 57)) stringByte -= 48;

- contracts/intentETH/BaseCustomizedOracle.sol

return 18;

- contracts/intentETH/RedStoneInETHOracle.sol

return (tokenPrice 10 * 18) / basePrice;

- contracts/intentETH/RedStoneInETHOracle.sol

return 18;

Score
Impact:
Likelihood:
Recommendation

To improve code maintainability, readability, and reduce the risk of potential errors, it is recommended to replace magic numbers with well-defined constants. By using constants, developers can provide clear and descriptive names for specific values, making the code easier to understand and maintain. Additionally, updating the values becomes more straightforward, as changes can be made in a single location, reducing the risk of errors and inconsistencies.


Remediation

ACKNOWLEDGED: The DappOs team acknowledged this issue.

4.25 Missing checks for address(0)

// Informational

Description

In some of the smart contracts in-scope, values are assigned to address state variables without checking whether the assigned address is the zero address (address(0)). This oversight can lead to unintended behavior and potential security vulnerabilities in the contract.

This behavior was identified in multiple (22) instances. The following is a non-exhaustive list:

- contracts/checker/WearChecker.sol

	        intentToken = _intentToken;

- contracts/core/pools/cachePool/CachePool.sol

	        intentToken = _intentToken;

- contracts/core/pools/cachePool/CachePool.sol

	        wearChecker = _wearChecker;

- contracts/core/pools/cachePool/CachePool.sol

	        intentTokenMinting = _intentTokenMinting;

- contracts/core/pools/cachePool/CachePool.sol

	        plugins[_executionPlugin] = isEnabled;

- contracts/core/pools/mainPool/MainPool.sol

	        intentToken = _intentToken;

- contracts/core/pools/mainPool/MainPoolConfiguration.sol

	        intentToken = _intentToken;

Score
Impact:
Likelihood:
Recommendation

To prevent unintended behavior and potential security vulnerabilities, it is essential to include checks for address(0) when assigning values to address state variables. This can be achieved by adding a simple check to ensure that the assigned address is not equal to address(0) before proceeding with the assignment.


Remediation

ACKNOWLEDGED: The DappOs team acknowledged this issue.

4.26 Floating pragma

// Informational

Description

Contracts should be deployed with the same compiler version and flags that they have been tested with thoroughly. Locking the pragma helps to ensure that contracts do not accidentally get deployed using, for example, an outdated compiler version that might introduce bugs that affect the contract system negatively.

In multiple (+50) locations it was identified the floating pragma, as follows:

pragma solidity ^0.8.20;
Score
Impact:
Likelihood:
Recommendation

Lock the pragma version and also consider known bugs (https://github.com/ethereum/solidity/releases) for the compiler version that is chosen.


Remediation

ACKNOWLEDGED: The DappOs team acknowledged this issue.

5. Automated Testing

Introduction

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.


slither(01)slither(02)

All issues identified by Slither were proved to be false positives, and therefore have not 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.

// Download the full report

* Use Google Chrome for best results

** Check "Background Graphics" in the print settings if needed

© Halborn 2024. All rights reserved.