Prepared by:
HALBORN
Last Updated 04/30/2025
Date of Engagement: April 21st, 2025 - April 23rd, 2025
100% of all REPORTED Findings have been addressed
All findings
4
Critical
1
High
0
Medium
0
Low
1
Informational
2
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.
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.
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.
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.
EXPLOITABILITY METRIC () | METRIC VALUE | NUMERICAL 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 |
IMPACT METRIC () | METRIC VALUE | NUMERICAL 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 |
SEVERITY COEFFICIENT () | COEFFICIENT VALUE | NUMERICAL VALUE |
---|---|---|
Reversibility () | None (R:N) Partial (R:P) Full (R:F) | 1 0.5 0.25 |
Scope () | Changed (S:C) Unchanged (S:U) | 1.25 1 |
Severity | Score Value Range |
---|---|
Critical | 9 - 10 |
High | 7 - 8.9 |
Medium | 4.5 - 6.9 |
Low | 2 - 4.4 |
Informational | 0 - 1.9 |
Critical
1
High
0
Medium
0
Low
1
Informational
2
Security analysis | Risk level | Remediation Date |
---|---|---|
Unhandled IBC Transfer Failures | Critical | Solved - 04/29/2025 |
Use of vulnerable dependency | Low | Solved - 04/29/2025 |
Unhandled Error Leading to Silent Failures | Informational | Acknowledged - 04/29/2025 |
Potential Inadequate Packet Validation in ICS4Wrapper | Informational | Acknowledged - 04/29/2025 |
//
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:
Claims yield tokens for each IBC escrow address using claimModuleYield
Transfers these tokens to external chains via IBC
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:
The Transfer
function only initiates the packet sending process and returns after the packet is sent from the source chain.
The actual delivery and acknowledgment happen asynchronously in subsequent blocks.
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:
The tokens remain locked in the escrow address with no recovery mechanism.
The TotalExternalYield
counter is already incremented, creating a state inconsistency.
There is no mechanism to retry failed transfers or recover the tokens.
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.
Implement a comprehensive solution to handle IBC transfer failures and enable recovery of stuck tokens:
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
}
Create a pending transfers tracking system and implement a recovery mechanism for failed transfers following those pending values.
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.
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.
//
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.
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.
SOLVED: The Noble team fixed this issue by updating the mentioned dependencies.
//
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)
}
// ...
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
}
ACKNOWLEDGED: The Noble team acknowledged this recommendation.
//
The ICS4Wrapper.SendPacket
function in the Noble Dollar module contains two informational issues related to packet validation that could potentially lead to unexpected behavior:
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.
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.
It is recommended the following:
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.
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.
ACKNOWLEDGED: The Noble team acknowledged this recommendation.
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.
// Download the full report
Noble Dollar v2 - IBC Support Update
* Use Google Chrome for best results
** Check "Background Graphics" in the print settings if needed