Noble Dollar v2 - IBC Support Update - Noble Assets


Prepared by:

Halborn Logo

HALBORN

Last Updated 04/30/2025

Date of Engagement: April 21st, 2025 - April 23rd, 2025

Summary

100% of all REPORTED Findings have been addressed

All findings

4

Critical

1

High

0

Medium

0

Low

1

Informational

2


1. Introduction

Noble engaged Halborn to conduct a security assessment on their smart contracts beginning on April 21st, 2025 and ending on April 25th, 2025. The security assessment was scoped to some changes made to a Cosmos module provided to the Halborn team. Commit hashes and further details can be found in the Scope section of this report.

2. Assessment Summary

The team at Halborn was provided nine days for the engagement and assigned one full-time security engineer to assessment the security of the merge requests. The security engineers are blockchain and smart-contract security experts with advanced penetration testing, smart-contract hacking, and deep knowledge of multiple blockchain protocols.

The purpose of this assessment is to:

    • Ensure that the Golang components operate as intended.

    • Identify potential security issues with the Cosmos application.


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

    • Implement IBC callback handlers and tracking system for pending transfers to recover tokens in case of transfer failures.

    • Update dependencies with public known issues/vulnerabilities.

    • Implement logic related to error handling.

    • Improve ICS4Wrapper packet validations.

3. 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 the custom modules. While manual testing is recommended to uncover flaws in logic, process, and implementation; automated testing techniques help enhance coverage of structures and can quickly identify items that do not follow security best practices. The following phases and associated tools were used throughout the term of the assessment :

    • Research into architecture and purpose.

    • Static Analysis of security for scoped repository, and imported functions. (e.g., staticcheck, gosec, unconvert, codeql, ineffassign and semgrep)

    • Manual Assessment for discovering security vulnerabilities on the codebase.

    • Ensuring the correctness of the codebase.

    • Dynamic Analysis of files and modules in scope.

4. Caveats

The security assessment is strictly limited to the files directly affected by the diff between the main (83a5546) and v2 (ffef804) branches. Only changes introduced or modified in this comparison are considered; any pre-existing vulnerabilities or issues outside of these specific files fall outside the scope of this review. Additionally, the audit does not encompass dependencies, configuration files, or runtime environments. As such, findings and recommendations pertain solely to the code and files that have been added, removed, or changed in this particular branch comparison.

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

Files and Repository
(a) Repository: dollar
(b) Assessed Commit ID: 83a5546
(c) Items in scope:
  • genesis.go
  • isc4_wrapper.go
  • keeper/keeper.go
↓ Expand ↓
Out-of-Scope: Third party dependencies and economic attacks.
Remediation Commit ID:
Out-of-Scope: New features/implementations after the remediation commit IDs.

7. Assessment Summary & Findings Overview

Critical

1

High

0

Medium

0

Low

1

Informational

2

Security analysisRisk levelRemediation Date
Unhandled IBC Transfer FailuresCriticalSolved - 04/29/2025
Use of vulnerable dependencyLowSolved - 04/29/2025
Unhandled Error Leading to Silent FailuresInformationalAcknowledged - 04/29/2025
Potential Inadequate Packet Validation in ICS4WrapperInformationalAcknowledged - 04/29/2025

8. Findings & Tech Details

8.1 Unhandled IBC Transfer Failures

//

Critical

Description

The Noble Dollar module's claimExternalYieldIBC function contains a critical vulnerability where it fails to properly handle potential IBC transfer failures, resulting in permanent loss of yield tokens and state inconsistencies in the chain.


The function is designed to distribute yield to recipients across IBC channels through a process that:

  1. Claims yield tokens for each IBC escrow address using claimModuleYield

  2. Transfers these tokens to external chains via IBC

  3. Updates the state by incrementing the total external yield counter

func (k *Keeper) claimExternalYieldIBC(ctx context.Context) error {
    provider := v2.Provider_IBC
    yieldRecipients, err := k.GetYieldRecipientsByProvider(ctx, provider)
    if err != nil {
        return errors.Wrap(err, "unable to get ibc yield recipients from state")
    }

    for channelId, yieldRecipient := range yieldRecipients {
        escrowAddress := transfertypes.GetEscrowAddress(transfertypes.PortID, channelId)
        yield, err := k.claimModuleYield(ctx, escrowAddress)
        if err != nil {
            return errors.Wrapf(err, "unable to claim yield for %s/%s", provider, channelId)
        }
        if !yield.IsPositive() {
            continue
        }

        timeout := uint64(k.header.GetHeaderInfo(ctx).Time.UnixNano()) + transfertypes.DefaultRelativePacketTimeoutTimestamp
        _, err = k.transfer.Transfer(ctx, &transfertypes.MsgTransfer{
            SourcePort:       transfertypes.PortID,
            SourceChannel:    channelId,
            Token:            sdk.NewCoin(k.denom, yield),
            Sender:           escrowAddress.String(),
            Receiver:         yieldRecipient,
            TimeoutHeight:    clienttypes.ZeroHeight(),
            TimeoutTimestamp: timeout,
            Memo:             "",
        })
        if err != nil {
            return errors.Wrapf(err, "unable to transfer yield for %s/%s", provider, channelId)
        }

        err = k.IncrementTotalExternalYield(ctx, provider, channelId, yield)
        if err != nil {
            return errors.Wrapf(err, "unable to increment total yield for %s/%s", provider, channelId)
        }

        k.logger.Info("claimed and transferred ibc yield", "amount", yield, "identifier", channelId)
    }

    return nil
}

The critical vulnerability is that the function assumes all IBC transfers will complete successfully once the Transfer function returns without error. However, in the IBC protocol, this assumption is flawed because:

  1. The Transfer function only initiates the packet sending process and returns after the packet is sent from the source chain.

  2. The actual delivery and acknowledgment happen asynchronously in subsequent blocks.

  3. Multiple failure scenarios can occur after the packet is sent:

    • The packet may time out if the destination chain doesn't process it within the timeout period.

    • The channel was closed.

    • The packet may be rejected by the destination chain due to validation errors.

    • Network partitions may prevent packet delivery.


When an IBC transfer fails after the packet has been sent:

  1. The tokens remain locked in the escrow address with no recovery mechanism.

  2. The TotalExternalYield counter is already incremented, creating a state inconsistency.

  3. There is no mechanism to retry failed transfers or recover the tokens.

  4. Future yield distributions continue to assume previous transfers were successful.


This design flaw creates several serious risks:

  • Permanent loss of yield tokens that should have been distributed to external recipients.

  • Progressive accumulation of invalid state as more transfers fail over time.

  • Discrepancies between the reported total external yield and the actual amount of yield successfully distributed.

  • No ability to audit or recover from failed transfers.


BVSS
Recommendation

Implement a comprehensive solution to handle IBC transfer failures and enable recovery of stuck tokens:


  1. Implement IBC callbacks to track transfer outcomes:

    // Add to ICS4Wrapper implementation
    func (k Keeper) OnAcknowledgementPacket(ctx sdk.Context, packet channeltypes.Packet, acknowledgement []byte) error {
        // Verify this is a yield distribution packet
        // If successful ack, confirm the transfer
        // If failed ack, record the failed transfer and return tokens to recovery pool
        return nil
    }
    
    func (k Keeper) OnTimeoutPacket(ctx sdk.Context, packet channeltypes.Packet) error {
        // Handle timeout by recording the failed transfer
        // Return tokens to recovery pool
        return nil
    }
  2. Create a pending transfers tracking system and implement a recovery mechanism for failed transfers following those pending values.

  3. Add administrative functions to manage recovery or let external users to claim the yield manually.


By implementing these changes, the module will be able to properly handle IBC transfer failures, prevent permanent token loss, maintain accurate state information, and provide mechanisms to recover and retry failed transfers.

Remediation Comment

SOLVED: The Noble team fixed this issue by implementing an IBC middleware to handle failed acknowledged and timeout packets, including a retry logic for future outbound packets.

Remediation Hash

8.2 Use of vulnerable dependency

//

Low

Description

The module uses a vulnerable dependency:

  • pkg:golang/golang.org/x/net@v0.34.0

  • pkg:golang/golang.org/x/crypto@v0.32.0

  • pkg:golang/github.com/ethereum/go-ethereum@1.14.13


Flagged by the nancy tool:

  • CVE-2025-22870: Misinterpretation of Input

  • CVE-2025-22869: Allocation of resources without limits or throttling.

  • CVE-2023-42319: Geth (aka go-ethereum) through 1.13.4, when --http --graphql is used, allows remote attackers to cause a denial of service (memory consumption and daemon hang) via a crafted GraphQL query.


Using vulnerable dependencies can compromise the security and stability of the protocol, exposing it to potential attacks, performance degradation, and operational disruptions.

BVSS
Recommendation

It is recommended to keep dependencies patched in order to reduce the risk of the system being attacked using known vulnerabilities. Run the security tools regularly and fix the warning.

Remediation Comment

SOLVED: The Noble team fixed this issue by updating the mentioned dependencies.

Remediation Hash

8.3 Unhandled Error Leading to Silent Failures

//

Informational

Description

In the Stats query handler of the queryServerV2 struct, there is an issue where errors from the math.NewIntFromString function are silently ignored. This function returns two values: the converted Int and a boolean indicating success or failure of the conversion. However, the current implementation discards the success indicator with the blank identifier (_).


func (k queryServerV2) Stats(ctx context.Context, req *v2.QueryStats) (*v2.QueryStatsResponse, error) {
    // ... other code omitted ...
    
    totalExternalYield := make(map[string]v2.QueryStatsResponse_ExternalYield)
    for key, rawAmount := range stats.TotalExternalYield {
        provider, identifier := v2.ParseYieldRecipientKey(key)

        chainId := "UNKNOWN"
        switch provider {
        case v2.Provider_IBC:
            chainId = GetIBCChainId(ctx, k.channel, identifier)
        }

        amount, _ := math.NewIntFromString(rawAmount)  // Error indicator is ignored

        totalExternalYield[key] = v2.QueryStatsResponse_ExternalYield{
            ChainId: chainId,
            Amount:  amount,
        }
    }
    
    // ... return response ...
}

When math.NewIntFromString fails to parse a string (for example, if it contains non-numeric characters or represents a value outside the supported range), it returns a zero value (Int{0}) and false for the second return value. By ignoring this second value with the blank identifier _, the code silently accepts invalid inputs.


This also applies to IncrementTotalExternalYield function in state.go.


func (k *Keeper) IncrementTotalExternalYield(ctx context.Context, provider v2.Provider, identifier string, amount math.Int) error {
	stats, err := k.Stats.Get(ctx)
	if err != nil {
		return err
	}
	if stats.TotalExternalYield == nil {
		stats.TotalExternalYield = make(map[string]string)
	}

	key := fmt.Sprintf("%s/%s", provider, identifier)

	totalExternalYield := math.ZeroInt()
	rawTotalExternalYield, exists := stats.TotalExternalYield[key]
	if exists {
		totalExternalYield, _ = math.NewIntFromString(rawTotalExternalYield)
	}

// ...

BVSS
Recommendation

Always check the boolean return value from math.NewIntFromString and handle error cases appropriately.


amount, ok := math.NewIntFromString(rawAmount)
if !ok {
    // Log the error but continue with zero amount
    k.logger.Error("failed to parse amount in Stats query", 
                   "key", key, 
                   "raw_amount", rawAmount)
    return err
}

Remediation Comment

ACKNOWLEDGED: The Noble team acknowledged this recommendation.

8.4 Potential Inadequate Packet Validation in ICS4Wrapper

//

Informational

Description

The ICS4Wrapper.SendPacket function in the Noble Dollar module contains two informational issues related to packet validation that could potentially lead to unexpected behavior:


  1. Insufficient Error Handling during Unmarshaling: The function attempts to unmarshal the incoming packet data into a FungibleTokenPacketData structure. However, if unmarshaling fails, instead of returning an error, it silently forwards the packet to the underlying ICS4Wrapper without additional validation:


func (w ICS4Wrapper) SendPacket(ctx sdk.Context, chanCap *capabilitytypes.Capability, sourcePort string, sourceChannel string, timeoutHeight clienttypes.Height, timeoutTimestamp uint64, data []byte) (sequence uint64, err error) {
    var packetData transfertypes.FungibleTokenPacketData
    if err := transfertypes.ModuleCdc.UnmarshalJSON(data, &packetData); err != nil {
        return w.ics4Wrapper.SendPacket(ctx, chanCap, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, data)
    }

    denom := w.dollarKeeper.GetDenom()
    if packetData.Denom == denom {
        enabled := w.dollarKeeper.HasYieldRecipient(ctx, v2.Provider_IBC, sourceChannel)
        if !enabled {
            return 0, fmt.Errorf("ibc transfers of %s are currently disabled on %s", denom, sourceChannel)
        }
    }

    return w.ics4Wrapper.SendPacket(ctx, chanCap, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, data)
}

This behavior means that non-ICS20 packets or malformed packets will automatically pass through without proper inspection. While this may be intentional for supporting other packet types, it bypasses any custom validation logic defined in this wrapper.


  1. No Validation for IBC-Traced Denominations: The current implementation only checks if packetData.Denom exactly matches the local native token denom. However, this doesn't account for IBC-traced denominations that follow the ICS-20 specification pattern (i.e., denominations in the format hash(port/channel)/denom).


For example, if the native denom is usdn, the function would validate transfers of usdn but would not validate transfers of IBC-traced forms like transfer/channel-1/usdn, which could still represent the same underlying asset returning through a different channel.


In the ICS-20 specification, a proper validation should consider the entire denomination trace to determine the original denomination and ensure all rules are properly enforced regardless of whether the token has traveled through other IBC channels.


BVSS
Recommendation

It is recommended the following:


  1. Improve Error Handling for Unmarshaling:

    • Consider logging unmarshaling errors to aid in debugging

    • Determine if non-ICS20 packets should be allowed, and if not, explicitly return errors for unsupported packet types.

  2. Add Support for IBC-Traced Denominations:

    • Implement proper parsing of IBC denomination traces to validate transfers of the native token that might have been routed through other chains.


These improvements would ensure that the module properly handles all packet types and enforces transfer restrictions correctly for all representations of the native token, whether in its original form or as an IBC-traced denomination.


Remediation Comment

ACKNOWLEDGED: The Noble team acknowledged this recommendation.

9. Automated Testing

Halborn used automated testing techniques to enhance coverage of certain areas of the scoped component. Among the tools used were staticcheck, gosec and Nancy. After Halborn verified all the files and scoped structures in the repository and was able to compile them correctly, these tools were leveraged on scoped structures. With these tools, Halborn can statically verify security related issues across the entire codebase.


The result of those automatic tools were analyzed, and the only identified issue Use of vulnerable dependency was described in the 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 2025. All rights reserved.