Skip to content

Conversation

@johnletey
Copy link
Member

@johnletey johnletey commented Jun 17, 2025

Summary by CodeRabbit

  • New Features

    • Introduced a yield-bearing ERC20 token with principal-based accounting and automatic yield accrual.
    • Added yield claiming functionality for token holders.
    • Enabled cross-chain interoperability and messaging support.
    • Added Hyperliquid integration variant.
  • Documentation

    • Added comprehensive contract documentation.
  • Tests

    • Added extensive test suite covering yield mechanics, transfers, claims, and upgrades.
  • Chores

    • Updated project configuration and dependencies.

✏️ Tip: You can customize this high-level summary in your review settings.

@johnletey johnletey self-assigned this Jun 17, 2025
@coderabbitai
Copy link

coderabbitai bot commented Jun 17, 2025

Walkthrough

This PR introduces a yield-bearing ERC20 token contract (NobleDollar) extending Hyperlane's HypERC20, with principal-based accounting and index-driven yield accrual. Supporting utilities handle safe integer conversions and index calculations. Comprehensive tests, documentation, project configuration, and a Hyperliquid variant complete the implementation.

Changes

Cohort / File(s) Summary
Configuration & Setup
.gitignore, contracts/foundry.toml, contracts/package.json
Project configuration: ignore cache, node_modules, and build artifacts; Foundry config with Hyperlane and OpenZeppelin remappings; npm package with dev dependencies and Hyperlane patch.
Core Token Contracts
contracts/src/NobleDollar.sol, contracts/src/HyperliquidNobleDollar.sol
NobleDollar: yield-bearing ERC20 extending HypERC20 with principal-based accounting, index tracking, yield accrual, and claim functionality; HyperliquidNobleDollar: Hyperliquid-specific variant with bridge integration.
Utility Libraries
contracts/utils/IndexingMath.sol, contracts/utils/UIntMath.sol
IndexingMath: index-based conversions between principal and present amounts with rounding controls; UIntMath: safe uint112/uint128 casts and min helper.
Testing & Documentation
contracts/test/NobleDollar.t.sol, contracts/README.md
Comprehensive test suite covering minting, yields, transfers, claims, upgrades, and edge cases; detailed README explaining yield mechanics, core functions, and security features.
Dependency Patches
contracts/patches/@hyperlane-xyz%2Fcore@9.0.15.patch
Hyperlane patches: replace Address.isContract with direct code.length check, adjust Ownable initialization, remove totalSupply parameter from HypERC20.initialize.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • IndexingMath rounding logic: Verify floor/ceil conversions, overflow safety in unchecked blocks, and consistency across principal/present/index calculations
  • NobleDollar._update() control flow: Trace all branches (mint, burn, yield accrual, yield payout, regular transfer) and principal/index state transitions
  • Test coverage depth: Validate scenarios for multi-user yield distribution, edge cases (uint112 max, zero principal), and upgrade mechanics
  • Hyperlane patch correctness: Confirm Address removal, Ownable init changes, and HypERC20.initialize signature alteration don't break downstream logic

Poem

🐰 A token born with yields divine,
Principals tracked with index's line,
Accruing wealth through cross-chain flight,
Hyperlane bridges day and night! 🌉
Claims cascade in perfect math—
Welcome to USDN's golden path! ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'feat: solidity contract' is extremely vague and generic. It does not convey meaningful information about the specific changes, which include implementing the NobleDollar yield-bearing token contract, adding utility libraries, configuration files, and comprehensive tests. Use a more descriptive title that captures the main feature, such as 'feat: add NobleDollar yield-bearing token with indexing and Hyperlane integration' or 'feat: implement USDN token with yield accrual and cross-chain support'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch contracts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@johnletey johnletey marked this pull request as ready for review July 9, 2025 09:44
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (2)
contracts/test/NobleDollar.t.sol (1)

33-35: Consider using dynamic addresses instead of hardcoded constants

Hardcoded addresses can make tests brittle and less maintainable. Consider using makeAddr() or deploying mock contracts.

-address constant MAILBOX = 0xc005dc82818d67AF737725bD4bf75435d065D239;
-address constant USER1 = 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045;
-address constant USER2 = 0xF2f1ACbe0BA726fEE8d75f3E32900526874740BB;
+address MAILBOX = makeAddr("mailbox");
+address USER1 = makeAddr("user1");  
+address USER2 = makeAddr("user2");
contracts/patches/@hyperlane-xyz%2Fcore@8.0.0.patch (1)

1-68: Recommend thorough security audit of this patch

This patch modifies critical security-related functionality in a third-party dependency. The changes to contract validation and ownership initialization could have significant security implications.

Consider:

  1. Contributing these changes upstream to Hyperlane
  2. Conducting a thorough security audit of the patch
  3. Documenting the rationale for each change
  4. Implementing comprehensive tests for the modified functionality
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3a538d3 and 362448a.

⛔ Files ignored due to path filters (1)
  • contracts/bun.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • .gitignore (1 hunks)
  • contracts/foundry.toml (1 hunks)
  • contracts/package.json (1 hunks)
  • contracts/patches/@hyperlane-xyz%2Fcore@8.0.0.patch (1 hunks)
  • contracts/src/NobleDollar.sol (1 hunks)
  • contracts/test/NobleDollar.t.sol (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (11)
.gitignore (1)

4-6: LGTM: Standard build artifacts and dependencies excluded

These additions are appropriate for a smart contract project using Foundry and npm dependencies.

contracts/package.json (1)

4-8: Security vulnerabilities check for dependencies

Verified that OpenZeppelin Contracts v5.3.0 and Hyperlane core v8.0.0 have no publicly disclosed security vulnerabilities as of July 2025.
Recommend enabling automated vulnerability alerts (e.g., dependabot, Snyk) to catch future advisories.

• File: contracts/package.json – @openzeppelin/contracts@5.3.0, @openzeppelin/contracts-upgradeable@5.3.0, @hyperlane-xyz/core@8.0.0

contracts/foundry.toml (1)

1-6: LGTM: Standard Foundry configuration with explicit remappings

The configuration follows best practices by disabling auto-detect remappings and using explicit ones for reproducible builds.

contracts/test/NobleDollar.t.sol (2)

109-110: Potential precision loss in principal calculation

The principal calculation after transfer shows 500055747208 instead of exact half. Verify this is expected behavior due to yield accrual effects.

Should the principal after 500k transfer be exactly 500055747208, or is there a rounding issue?


90-95: No changes needed for yield calculation logic

The yield() implementation computes

expectedBalance = principal * index / 1e12;
yield = expectedBalance > currentBalance ? expectedBalance - currentBalance : 0;

With a principal of 1e12 and an index of 1000111506849,
expectedBalance = 1e12 × 1000111506849 / 1e12 = 1000111506849,
so yield = 1000111506849 − 1000000000000 = 111506849, matching the test. All math is correct.

contracts/patches/@hyperlane-xyz%2Fcore@8.0.0.patch (3)

55-65: Verify removal of initial token minting

The removal of _totalSupply parameter and _mint(msg.sender, _totalSupply) significantly changes the token initialization behavior. Ensure this aligns with the intended token economics.

This change means tokens won't be minted to the deployer during initialization. Verify this is intentional for the NobleDollar use case and doesn't break existing functionality.


35-40: Ownership initialization via __Ownable_init(_owner) is correct and safe

The one-step call to __Ownable_init(_owner) atomically sets the intended owner, avoids an intermediate ownership window, and uses fewer storage writes. This matches OpenZeppelin’s recommended pattern in v5.x+ and does not introduce any security regressions. No further changes are needed.


25-30: No additional security risk: code.length > 0 is functionally equivalent to Address.isContract
Both approaches perform the same EVM bytecode‐length check and share identical limitations (during construction and post‐selfdestruct). No new vulnerabilities are introduced by replacing Address.isContract with a direct code.length check.

Likely an incorrect or invalid review comment.

contracts/src/NobleDollar.sol (3)

68-91: Well-implemented EIP-7201 storage pattern and initialization logic.

The namespaced storage layout follows best practices for upgradeable contracts, and the index initialization to 1e12 provides appropriate precision for yield calculations.


93-129: Correct implementation of view functions and yield calculation.

The yield calculation properly handles edge cases and follows the documented formula. The arithmetic ordering (multiply before divide) prevents precision loss.


139-148: Clean and secure yield claiming implementation.

The function correctly validates available yield before transfer and emits appropriate events for tracking.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
contracts/README.md (2)

1-6: Minor Markdown style/grammar polish

Small nits that improve readability:

-# USDN Solidity Contract
-
-## Overview
-
-The `NobleDollar` contract is the **Solidity implementation** …
+# USDN Solidity Contract
+
+## Overview
+
+The `NobleDollar` contract is the **Solidity implementation** …

– Remove the extra blank line after the top-level header, and ensure only one space after punctuation.
Not blocking but worth cleaning up before publishing to docs.


86-93: Security section could reference standard ERC-20 risks

You list rounding protection & transfer restrictions, but common ERC-20 pitfalls such as re-entrancy during claim() and upgrade-ability considerations (if behind a proxy) are absent. A short note here will help auditors & integrators.

No code change required; just add a bullet.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 362448a and ac6e614.

📒 Files selected for processing (1)
  • contracts/README.md (1 hunks)
🧰 Additional context used
🪛 LanguageTool
contracts/README.md

[grammar] ~1-~1: Use correct spacing
Context: # USDN Solidity Contract ## Overview The NobleDollar contract is...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~3-~3: Use proper spacing conventions.
Context: # USDN Solidity Contract ## Overview The NobleDollar contract is the **Soli...

(QB_NEW_EN_OTHER_ERROR_IDS_000007)


[grammar] ~5-~5: There might be a mistake here.
Context: ...ble Dollar to Ethereum Virtual Machine (EVM) compatible chains through Hyperlane. #...

(QB_NEW_EN_OTHER)


[grammar] ~5-~5: Use correct spacing
Context: ...Machine (EVM) compatible chains through Hyperlane. ## Key Features ### Yield-Bearing Token M...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~7-~7: Use correct spacing
Context: ...tible chains through Hyperlane. ## Key Features ### Yield-Bearing Token Mechanics - **Princ...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~10-~10: There might be a mistake here.
Context: ...principal amounts separately from token balances - Automatic yield accrual: Yield is...

(QB_NEW_EN_OTHER)


[grammar] ~11-~11: There might be a mistake here.
Context: ...lly to all token holders based on their principal - Claimable yield: Users can claim ...

(QB_NEW_EN_OTHER)


[grammar] ~12-~12: There might be a mistake here.
Context: ...accumulated yield through the claim() function - Index-based calculations: Uses a ...

(QB_NEW_EN_OTHER)


[grammar] ~13-~13: There might be a mistake here.
Context: ... scaled index (1e12) to calculate yield distributions ### Cross-Chain Interoperability - **Hyperl...

(QB_NEW_EN_OTHER)


[grammar] ~16-~16: There might be a mistake here.
Context: ...rom HypERC20 for seamless cross-chain transfers - Cross-chain yield distribution: R...

(QB_NEW_EN_OTHER)


[grammar] ~17-~17: There might be a mistake here.
Context: ...bution**: Receives yield from Noble via Hyperlane - Unified token experience: Maintai...

(QB_NEW_EN_OTHER)


[grammar] ~18-~18: There might be a problem here.
Context: ...nt yield mechanics across all supported chains ## Architecture The contract extends Hyperlane's `HypER...

(QB_NEW_EN_MERGED_MATCH)


[grammar] ~22-~22: Use correct spacing
Context: ... Hyperlane's HypERC20 token router to provide: 1. ERC20 functionality with additional y...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~27-~27: Use correct spacing
Context: ...dates** when new yield is received from Noble ## Core Functions ### View Functions - `...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~29-~29: Use correct spacing
Context: ...w yield is received from Noble ## Core Functions ### View Functions - index(): Returns th...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~31-~31: Use correct spacing
Context: ...from Noble ## Core Functions ### View Functions - index(): Returns the current yield index used ...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~33-~33: There might be a mistake here.
Context: ...eturns the current yield index used for calculations - totalPrincipal(): Returns the total...

(QB_NEW_EN_OTHER)


[grammar] ~34-~34: There might be a mistake here.
Context: ...s the total principal amount across all holders - principalOf(address): Returns the p...

(QB_NEW_EN_OTHER)


[grammar] ~35-~35: There might be a mistake here.
Context: ...rns the principal amount for a specific account - yield(address): Calculates claimabl...

(QB_NEW_EN_OTHER)


[grammar] ~36-~36: There might be a mistake here.
Context: ...lculates claimable yield for a specific account ### User Functions - claim(): Claims all...

(QB_NEW_EN_OTHER)


[grammar] ~38-~38: There might be a problem here.
Context: ... yield for a specific account ### User Functions - claim(): Claims all available yield for the caller - St...

(QB_NEW_EN_MERGED_MATCH)


[grammar] ~40-~40: There might be a mistake here.
Context: ...()`: Claims all available yield for the caller - Standard ERC20 functions (transfer, approve, etc...

(QB_NEW_EN_OTHER)


[grammar] ~41-~41: There might be a mistake here.
Context: ...ansfer, approve, etc.) with yield-aware accounting ## The _update Function The _update f...

(QB_NEW_EN_OTHER)


[grammar] ~43-~43: Use correct spacing
Context: ...ield-aware accounting ## The _update Function The _update function is the core of t...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~45-~45: Use correct spacing
Context: ...o handle principal accounting and yield distribution. mermaid flowchart LR Start(["Start Transfer Logic"]) --> Super["ERC20 Transfer Logic"] Super --> CheckFromThis{"from == contract?"} CheckFromThis -- Yes --> SkipYieldPayout["Skip Principal Update<br><i>yield payout</i>"] SkipYieldPayout --> End1(["End"]) CheckFromThis -- No --> CheckToThis{"to == contract?"} CheckToThis -- Yes --> CheckMint{"from == 0x0?"} CheckMint -- Yes --> UpdateIndex["Update Index<br><i>yield accrual</i>"] UpdateIndex --> End2(["End"]) CheckMint -- No --> Revert["Revert"] CheckToThis -- No --> CalcPrincipal["Calculate Principal<br><i>round up</i>"] CalcPrincipal --> FromNotZero{"from != zero_address?"} FromNotZero -- Yes --> SubFromPrincipal["Subtract from Principal"] FromNotZero -- No --> AddToTotalPrincipal["Add to Total Principal"] SubFromPrincipal --> ToNotZero{"to != zero_address?"} AddToTotalPrincipal --> ToNotZero ToNotZero -- Yes --> CheckMint2{"from == zero_address?"} CheckMint2 -- Yes --> RecalcPrincipal["Recalculate Principal<br><i>round down</i>"] CheckMint2 -- No --> NoOp1["No Change"] RecalcPrincipal --> AddToPrincipal["Add to Principal"] NoOp1 --> AddToPrincipal AddToPrincipal --> End3(["End"]) ToNotZero -- No --> SubFromTotalPrincipal["Subtract from Total Principal"] SubFromTotalPrincipal --> End4(["End"]) ### Function Behavior Breakdown 1. **Yield...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~74-~74: Use correct spacing
Context: ...nd4(["End"]) ``` ### Function Behavior Breakdown 1. Yield Payout (from == contract): Wh...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~76-~76: There might be a mistake here.
Context: ...ield to users, no principal updates are needed 2. Yield Accrual (`to == contract &...

(QB_NEW_EN_OTHER)


[grammar] ~78-~78: There might be a mistake here.
Context: ...tes the global index based on new total supply - Emits IndexUpdated event 3. **In...

(QB_NEW_EN_OTHER)


[grammar] ~79-~79: There might be a mistake here.
Context: ... total supply - Emits IndexUpdated event 3. Invalid Transfer (`to == contrac...

(QB_NEW_EN_OTHER)


[grammar] ~80-~80: There might be a mistake here.
Context: ...ers from sending tokens directly to the contract 4. Regular Operations: For normal t...

(QB_NEW_EN_OTHER)


[grammar] ~82-~82: There might be a mistake here.
Context: ...es principal using ceiling division for accuracy - Updates sender/recipient principal...

(QB_NEW_EN_OTHER)


[grammar] ~83-~83: There might be a mistake here.
Context: ... - Updates sender/recipient principal balances - Maintains total principal accounti...

(QB_NEW_EN_OTHER)


[grammar] ~84-~84: There might be a mistake here.
Context: ...balances - Maintains total principal accounting ## Security Features - **Rounding Protect...

(QB_NEW_EN_OTHER)


[grammar] ~86-~86: Use correct spacing
Context: ...total principal accounting ## Security Features - Rounding Protection: Uses ceiling div...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~88-~88: There might be a mistake here.
Context: ...s ceiling division to prevent principal manipulation - Transfer Restrictions: Prevents u...

(QB_NEW_EN_OTHER)


[grammar] ~89-~89: There might be a mistake here.
Context: ... unauthorized transfers to the contract address - Index Validation: Ensures yield c...

(QB_NEW_EN_OTHER)


[grammar] ~90-~90: There might be a problem here.
Context: ...d calculations remain consistent across operations ## Events - IndexUpdated: Emitted when yield is accrued and the...

(QB_NEW_EN_MERGED_MATCH)


[grammar] ~94-~94: There might be a mistake here.
Context: ... when yield is accrued and the index is updated - YieldClaimed: Emitted when a user c...

(QB_NEW_EN_OTHER)


[grammar] ~95-~95: There might be a mistake here.
Context: ...imed`: Emitted when a user claims their yield - Standard ERC20 events for transfers a...

(QB_NEW_EN_OTHER)


[grammar] ~96-~96: There might be a problem here.
Context: ...Standard ERC20 events for transfers and approvals ## Deployment The contract is deployed with: - **Deci...

(QB_NEW_EN_MERGED_MATCH)


[grammar] ~103-~103: There might be a problem here.
Context: ... 1e12 (representing 1.0 with 12 decimal places) ## Integration To integrate with the NobleDollar contr...

(QB_NEW_EN_MERGED_MATCH)


[grammar] ~107-~107: Use correct spacing
Context: ...tion To integrate with the NobleDollar contract: 1. Standard ERC20 Integration: Use stand...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~109-~109: There might be a mistake here.
Context: ...Use standard ERC20 interfaces for basic operations 2. Yield Monitoring: Listen for `In...

(QB_NEW_EN_OTHER)


[grammar] ~110-~110: There might be a mistake here.
Context: ...or IndexUpdated events to track yield accrual 3. Yield Claims: Call `yield(addres...

(QB_NEW_EN_OTHER)


[grammar] ~111-~111: There might be a mistake here.
Context: ...imable amounts and claim() to collect yield 4. Cross-Chain Operations: Utilize ...

(QB_NEW_EN_OTHER)


[grammar] ~112-~112: There might be a problem here.
Context: ...ize Hyperlane's routing for cross-chain transfers ## License Copyright 2025 NASD Inc. All rights res...

(QB_NEW_EN_MERGED_MATCH)


[grammar] ~116-~116: Use correct spacing
Context: ...se Copyright 2025 NASD Inc. All rights reserved. Licensed under the Apache License, Vers...

(QB_NEW_EN_OTHER_ERROR_IDS_5)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build

Comment on lines +114 to +118
## License

Copyright 2025 NASD Inc. All rights reserved.

Licensed under the Apache License, Version 2.0.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Contradictory license statements – pick one licensing model

“All rights reserved” is incompatible with distribution under the Apache 2.0 license.
Either (a) drop the Apache clause and keep proprietary rights, or (b) keep Apache 2.0 and delete the “all rights reserved” sentence (recommended for open-source).

-## License
-
-Copyright 2025 NASD Inc. All rights reserved.
-
-Licensed under the Apache License, Version 2.0.
+## License
+
+Copyright © 2025 NASD Inc.
+
+This project is licensed under the Apache License, Version 2.0 – see the `LICENSE` file for details.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
## License
Copyright 2025 NASD Inc. All rights reserved.
Licensed under the Apache License, Version 2.0.
## License
Copyright © 2025 NASD Inc.
This project is licensed under the Apache License, Version 2.0 – see the `LICENSE` file for details.
🧰 Tools
🪛 LanguageTool

[grammar] ~116-~116: Use correct spacing
Context: ...se Copyright 2025 NASD Inc. All rights reserved. Licensed under the Apache License, Vers...

(QB_NEW_EN_OTHER_ERROR_IDS_5)

🤖 Prompt for AI Agents
In contracts/README.md around lines 114 to 118, the license section contains
contradictory statements by including both "All rights reserved" and the Apache
License 2.0. To fix this, choose one licensing model: either remove the "All
rights reserved" phrase to comply with the Apache 2.0 open-source license or
remove the Apache License statement to maintain proprietary rights. The
recommended approach is to keep the Apache License 2.0 and delete the "All
rights reserved" sentence.

@johnletey johnletey changed the title feat: contracts feat: solidity contract Jul 9, 2025
johnletey and others added 2 commits August 11, 2025 04:36
Co-authored-by: stepit <48993133+0xstepit@users.noreply.github.com>
Co-authored-by: mesozoic-technology <system@mesozoic.technology>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
contracts/src/NobleDollar.sol (2)

18-18: Thanks for pinning the compiler to 0.8.20

Aligns with EIP-7201 guidance and prior review feedback; good call.


187-197: Zero-principal yield accrual: make revert reason explicit here (avoid library DivisionByZero)

Right now, with totalPrincipal == 0, this path will revert from IndexingMath with DivisionByZero. Consider guarding and reverting with a domain-specific error for clearer UX/logs, consistent with prior review feedback.

Apply this diff:

-        if (to == address(this)) {
+        if (to == address(this)) {
             uint128 oldIndex = $.index;
-
-            $.index = IndexingMath.getIndexRoundedDown(totalSupply(), $.totalPrincipal);
+            if ($.totalPrincipal == 0) {
+                // Prevent yield accrual before any principal exists to avoid ambiguous DivisionByZero.
+                revert InvalidTransfer();
+            }
+            $.index = IndexingMath.getIndexRoundedDown(totalSupply(), $.totalPrincipal);
 
             emit IndexUpdated(oldIndex, $.index, $.totalPrincipal, value);
 
             return;
         }
🧹 Nitpick comments (6)
contracts/utils/UIntMath.sol (1)

21-24: Nit: wording in NatSpec (“casted”)

Minor grammar polish for clarity.

Apply this diff to tighten the wording:

-     * @notice Casts a uint256 value to a uint112, ensuring that it is less than or equal to the maximum uint112 value.
+     * @notice Casts a uint256 value to a uint112, ensuring it is <= type(uint112).max.
      * @param  n The value to cast.
-     * @return The value casted to uint112.
+     * @return The value cast to uint112.
-     * @notice Casts a uint256 value to a uint128, ensuring that it is less than or equal to the maximum uint128 value.
+     * @notice Casts a uint256 value to a uint128, ensuring it is <= type(uint128).max.
      * @param  n The value to cast.
-     * @return The value casted to uint128.
+     * @return The value cast to uint128.

Also applies to: 31-34

contracts/utils/IndexingMath.sol (3)

46-51: Nit: comment references to non-existent helpers

The NOTE references multiply112By128Down/Up which aren’t present in this library anymore. This can mislead maintainers.

Apply this diff to update the comment:

-            // NOTE: While `uint256(presentAmount) * EXP_SCALED_ONE` can technically overflow, these divide/multiply functions are
-            //       only used for the purpose of principal/present amount calculations for continuous indexing, and
-            //       so for an `presentAmount` to be large enough to overflow this, it would have to be a possible result of
-            //       `multiply112By128Down` or `multiply112By128Up`, which would already satisfy
-            //       `uint256(presentAmount) * EXP_SCALED_ONE < type(uint240).max`.
+            // NOTE: While `presentAmount * EXP_SCALED_ONE` can technically overflow, in this system `presentAmount`
+            //       derives from principal/index-based calculations bounded by 112/128-bit types, keeping the product
+            //       below ~2^240. Under these bounds, the multiplication is safe in practice.

65-70: Nit: drifted comment, same as above

Mirror the wording fix in the rounded-up variant.

Apply this diff:

-            // NOTE: While `uint256(presentAmount) * EXP_SCALED_ONE` can technically overflow, these divide/multiply functions are
-            //       only used for the purpose of principal/present amount calculations for continuous indexing, and
-            //       so for an `presentAmount` to be large enough to overflow this, it would have to be a possible result of
-            //       `multiply112By128Down` or `multiply112By128Up`, which would already satisfy
-            //       `uint256(presentAmount) * EXP_SCALED_ONE < type(uint240).max`.
+            // NOTE: See overflow note in the rounded-down variant; under 112/128-bit bounds this product stays < 2^240.

96-102: Micro-nit: remove redundant qualification

Within the same library, referencing EXP_SCALED_ONE without IndexingMath. reads cleaner.

Apply this diff:

-            return UIntMath.safe128((presentAmount * IndexingMath.EXP_SCALED_ONE) / principal);
+            return UIntMath.safe128((presentAmount * EXP_SCALED_ONE) / principal);
contracts/src/NobleDollar.sol (2)

144-152: Consider nonReentrant on claim() if hooks are invoked in _update

If HypERC20’s _update triggers hooks that could reenter, wrap claim with nonReentrant. While balance updates happen in super._update first (mitigating double-claim), defensive hardening is low-cost.

Apply this diff to the function:

-function claim() public {
+function claim() public nonReentrant {
     uint256 amount = yield(msg.sender);
 
     if (amount == 0) revert NoClaimableYield();
 
     _update(address(this), msg.sender, amount);
 
     emit YieldClaimed(msg.sender, amount);
 }

Additionally, add the following (outside the selected lines) if not already present:

// At top of file:
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

// Update the contract declaration:
contract NobleDollar is HypERC20, ReentrancyGuard {
    // ...
}

176-178: Nit: zero-address no-op branch may be unnecessary

OpenZeppelin-style _update is typically called with exactly one zero address (mint or burn), not both. If this branch is never hit, consider removing to save a few bytes and reduce cognitive load.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d9935f0 and 75f5b61.

📒 Files selected for processing (3)
  • contracts/src/NobleDollar.sol (1 hunks)
  • contracts/utils/IndexingMath.sol (1 hunks)
  • contracts/utils/UIntMath.sol (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: prepare
  • GitHub Check: build
🔇 Additional comments (9)
contracts/utils/UIntMath.sol (3)

25-28: LGTM: safe112() is correct and gas-cheap

Bounds check then cast is correct; no unnecessary branches.


35-38: LGTM: safe128() mirrors safe112() correctly

Consistent pattern with appropriate error.


1-3: Verify license compatibility across the repo

This file is GPL-3.0, whereas NobleDollar.sol is Apache-2.0. Mixing GPL and Apache may impose GPL obligations downstream. If the intent is permissive licensing, consider aligning licenses or explicitly documenting the compatibility stance.

Would you like me to scan the repo to inventory SPDX headers and propose a unification plan?

contracts/utils/IndexingMath.sol (2)

15-15: LGTM: EXP_SCALED_ONE choice and type

1e12 scaling with a small-width type is reasonable for index math; it keeps bounds tight without risking overflow in the provided operations.


30-34: LGTM: getPresentAmountRoundedDown()

The unchecked mul/div is safe under your 112/128 constraints; no external dependency; minimal gas.

contracts/src/NobleDollar.sol (4)

126-134: LGTM: yield() accounting

Uses floor present value vs actual balance; non-negative and resistant to rounding dust.


80-86: LGTM: EIP-7201 storage location

Well-formed slot derivation and tight packing of index/totalPrincipal; mapping keying looks good.


1-17: Verify license alignment with dependencies

This file is Apache-2.0 while utility libraries in this PR are GPL-3.0. If you intend permissive licensing for the whole artifact, mixing licenses can be problematic. Either align SPDX across the repo or document the rationale.

Do you want me to inventory SPDX headers and propose a consistent set (e.g., all Apache-2.0)?


88-92: Verify HypERC20 constructor and initialize signatures

HypERC20.sol is imported from the external @hyperlane/token package, so we need to ensure that the parameters passed in NobleDollar.sol match the actual implementation:

  • Confirm in node_modules/@hyperlane/token/HypERC20.sol (or your locked dependency) that the constructor signature is:

    constructor(uint8 _decimals, uint32 _localDomain, address _mailbox)

    and that 6 and 1 correspond to your intended decimals and localDomain.

  • Confirm that the initialize function in the same file is declared as:

    function initialize(
      string memory name,
      string memory symbol,
      address hook_,
      address ism_,
      address caller_
    ) public

    so that super.initialize("Noble Dollar", "USDN", hook_, ism_, msg.sender) matches exactly.

A mismatch here (e.g., different types or parameter order) will lead to a misconfigured token across domains. Please verify these signatures in your installed @hyperlane/token package.

Co-authored-by: toninorair <antonina.norair@gmail.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
contracts/test/NobleDollar.t.sol (2)

36-54: Past review comment already addressed this concern.

The hardcoded address in the proxy initialization (line 43) was previously flagged. Based on the past review comments, this appears to have been addressed or acknowledged.


56-149: Past review comments already addressed these concerns.

Previous reviews flagged:

  1. The hardcoded hex payloads lacking documentation (lines 58-62, 79-83, etc.)
  2. Missing negative test cases

The negative test cases concern has been addressed with additional test functions (lines 151-519). The hex payload documentation suggestion remains from past reviews.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 75f5b61 and 8785241.

📒 Files selected for processing (4)
  • contracts/README.md (1 hunks)
  • contracts/src/NobleDollar.sol (1 hunks)
  • contracts/test/NobleDollar.t.sol (1 hunks)
  • contracts/utils/UIntMath.sol (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • contracts/utils/UIntMath.sol
  • contracts/README.md
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build
  • GitHub Check: prepare
🔇 Additional comments (16)
contracts/test/NobleDollar.t.sol (8)

151-169: LGTM!

Properly validates the InvalidTransfer error when attempting to transfer tokens to the contract from a non-zero address.


171-192: LGTM!

Properly validates the InvalidTransfer error for the transferFrom path, ensuring comprehensive coverage of transfer methods.


194-214: LGTM!

Correctly validates both scenarios where NoClaimableYield should be thrown: when no yield has accrued and when an account with zero balance attempts to claim.


216-239: Verify expected overflow behavior.

Line 238 expects the mint to silently fail (mintSuccess == false) when attempting to mint beyond uint112.max. Confirm whether the contract is designed to:

  1. Silently fail on overflow (current expectation), or
  2. Revert with an explicit error

If explicit reverts are preferred, update the test to use vm.expectRevert and ensure IndexingMath.getPrincipalAmountRoundedDown or related code properly reverts on overflow.


241-274: LGTM!

Correctly validates that when the index doubles due to yield, subsequent mints receive proportionally less principal, demonstrating proper index-based accounting.


276-433: Comprehensive yield claim coverage.

These tests effectively validate:

  • Single user claiming 100% yield
  • Multiple users claiming proportional yields
  • Yield claims after transfers with principal adjustments
  • Proper use of assertApproxEqAbs for rounding tolerance

435-456: LGTM!

Properly validates the edge case where yield accrual occurs with zero total principal, confirming the index remains unchanged and supply increases correctly.


458-518: LGTM!

Comprehensive test of burning behavior via transferRemote, validating:

  • Correct principal reduction after burn
  • Index progression with reduced principal base
  • Yield claims with updated principal values
contracts/src/NobleDollar.sol (8)

1-48: LGTM!

License, pragma version (0.8.30), and imports are properly configured. The pinned Solidity version meets EIP-7201 requirements.


50-86: LGTM!

Error definitions, event declarations, and ERC7201 storage pattern are correctly implemented:

  • Storage struct uses appropriately sized types (uint128 for index, uint112 for principals)
  • Storage location calculation follows ERC7201 specification
  • Events are properly indexed

88-96: Verify if initialize should be virtual.

The initialize function is marked virtual, allowing derived contracts to override initialization logic. Given the critical nature of index initialization for yield accounting, confirm whether allowing overrides is intentional.

If overriding is not needed, consider removing virtual to prevent unintended modifications to initialization logic.


98-111: LGTM!

Simple, correctly implemented view functions for accessing storage values.


113-136: LGTM!

Correctly calculates claimable yield as the difference between expected balance (principal × index) and current balance, properly handling the case where current balance exceeds expected balance.


138-155: LGTM - DOS prevention pattern.

The min() operation on line 148 correctly prevents DOS scenarios where multiple users claim yield simultaneously and the contract balance is insufficient for all claims. Partial claims are supported, with yield() automatically adjusting to reflect remaining claimable amounts.


176-225: Well-structured principal accounting logic.

The _update function correctly handles all transfer scenarios with appropriate principal adjustments:

  • Yield payouts: no principal updates (line 183)
  • Yield accrual: updates index only (lines 192-202), with proper zero-principal guard (line 193)
  • Minting: creates new principal (lines 204-211)
  • Transfers/burning: adjusts principal with safe underflow protection (lines 213-224)

The rounding strategy consistently favors the protocol (principal rounded down on mint, up on burn/transfer) to prevent insolvency through rounding exploits, as documented in line 174.


213-216: Verified underflow safety for principal deduction
getSafePrincipalAmountRoundedUp clamps the computed amount to fromPrincipal, ensuring no underflow occurs.

assertEq(usdn.balanceOf(USER1), 15e11, "USER1 should have 1.5M balance after claiming yield");

// USER2 yield claim should fail
vm.expectRevert(abi.encode(NobleDollar.NoClaimableYield.selector));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix revert expectation encoding.

Use abi.encodeWithSelector instead of abi.encode for consistency with other tests (e.g., lines 165, 188, 204).

Apply this diff:

-        vm.expectRevert(abi.encode(NobleDollar.NoClaimableYield.selector));
+        vm.expectRevert(abi.encodeWithSelector(NobleDollar.NoClaimableYield.selector));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
vm.expectRevert(abi.encode(NobleDollar.NoClaimableYield.selector));
vm.expectRevert(abi.encodeWithSelector(NobleDollar.NoClaimableYield.selector));
🤖 Prompt for AI Agents
In contracts/test/NobleDollar.t.sol around line 402, the test uses
vm.expectRevert(abi.encode(NobleDollar.NoClaimableYield.selector)); — change
this to use abi.encodeWithSelector(NobleDollar.NoClaimableYield.selector) to
match the encoding style used elsewhere; replace the abi.encode call with
abi.encodeWithSelector so the revert expectation encodes the selector correctly
and consistently with lines 165/188/204.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
contracts/test/NobleDollar.t.sol (1)

365-433: Fix revert expectation encoding on line 402.

Line 402 uses abi.encode(NobleDollar.NoClaimableYield.selector) which is inconsistent with the pattern used elsewhere in the test suite (lines 163, 187, 204, 210). For parameterless errors, use the bare selector; for consistency with encoded expectations, use abi.encodeWithSelector.

Apply this diff:

-        vm.expectRevert(abi.encode(NobleDollar.NoClaimableYield.selector));
+        vm.expectRevert(NobleDollar.NoClaimableYield.selector);

Or if you prefer explicit encoding:

-        vm.expectRevert(abi.encode(NobleDollar.NoClaimableYield.selector));
+        vm.expectRevert(abi.encodeWithSelector(NobleDollar.NoClaimableYield.selector));

Test logic: The test itself correctly validates that yield attribution persists across transfers and is distributed proportionally based on principal after subsequent yield accruals.

🧹 Nitpick comments (2)
contracts/test/NobleDollar.t.sol (2)

34-52: Use MAILBOX constant consistently.

Line 41 uses a hardcoded address 0x9e6B1022bE9BBF5aFd152483DAD9b88911bC8611 instead of the MAILBOX constant defined at line 30. For clarity and maintainability, use the constant unless this address represents a different entity (in which case, define a new named constant).

Apply this diff:

         abi.encodeWithSelector(
             NobleDollar.initialize.selector,
-            address(0x9e6B1022bE9BBF5aFd152483DAD9b88911bC8611),
+            MAILBOX,
             address(new NoopIsm())
         )

If the address is intentionally different, add a comment explaining why.


54-147: Consider extracting a helper for mailbox payloads.

The test comprehensively validates the happy path through multiple yield cycles. However, the hardcoded hex payloads (e.g., lines 59, 80, 131) remain opaque. While functional, a helper function to construct these payloads would significantly improve maintainability and readability.

Example helper:

function _buildMintPayload(address recipient, uint256 amount) internal pure returns (bytes memory) {
    // Construct and return the encoded payload with comments
    // ...
}

This would make tests like:

bytes memory mintPayload = _buildMintPayload(USER1, 1e12);

Much clearer than raw hex.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8785241 and 225fdc1.

⛔ Files ignored due to path filters (1)
  • contracts/bun.lock is excluded by !**/*.lock
📒 Files selected for processing (5)
  • contracts/foundry.toml (1 hunks)
  • contracts/package.json (1 hunks)
  • contracts/patches/@hyperlane-xyz%2Fcore@9.0.9.patch (1 hunks)
  • contracts/src/NobleDollar.sol (1 hunks)
  • contracts/test/NobleDollar.t.sol (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • contracts/package.json
  • contracts/foundry.toml
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: prepare
  • GitHub Check: build
🔇 Additional comments (13)
contracts/patches/@hyperlane-xyz%2Fcore@9.0.9.patch (3)

9-30: Address.isContract replacement is acceptable in this context.

The past review raised concerns about replacing Address.isContract with direct code.length checks. While Address.isContract does provide additional safety (clearer intent, handles edge cases), the simplified check is sufficient here because:

  • These modifiers (onlyContract, onlyContractOrNull) are used during initialization and configuration, not during constructor execution
  • The check _contract.code.length > 0 reliably detects deployed contracts in post-deployment contexts
  • This pattern is widely used in production contracts

However, ensure that no code paths attempt to validate contracts during their construction phase, where code.length would incorrectly return 0.


31-40: LGTM: Ownership initialization updated to modern pattern.

The change correctly updates to pass _owner directly to __Ownable_init(_owner) instead of calling _transferOwnership(_owner) separately. This aligns with newer OpenZeppelin patterns and eliminates redundant operations.


43-67: LGTM: Initialize signature simplified appropriately.

Removing the _totalSupply parameter and initial _mint call is appropriate for the yield-bearing token architecture introduced in this PR. The NobleDollar contract handles minting through cross-chain messages with index-based accounting, making initial supply minting at initialization inappropriate.

This is a breaking API change to HypERC20, but it aligns with the new use case.

contracts/src/NobleDollar.sol (5)

1-85: LGTM: Well-structured contract foundation.

The contract header, imports, and storage pattern are well-implemented:

  • Solidity version pinned at 0.8.30 as recommended
  • EIP-7201 storage location pattern correctly implemented
  • Type sizes are appropriate: uint128 for index (scaled 1e12) and uint112 for principal/amounts (max ~5.2e33 base units = 5.2e27 tokens with 6 decimals)
  • Clean separation of concerns with dedicated math libraries

87-95: LGTM: Correct proxy initialization pattern.

The constructor and initializer are properly structured:

  • _disableInitializers() prevents direct initialization of the implementation
  • Initial index set to 1e12 establishes the correct 1:1 starting ratio
  • Passes appropriate parameters to HypERC20 (6 decimals, domain 1)

97-135: LGTM: Well-documented view functions.

The getter functions are straightforward and correct. The yield() function deserves special mention:

  • Comprehensive documentation explaining the calculation and edge cases
  • Correct logic: max(0, expectedBalance - currentBalance)
  • Uses getPresentAmountRoundedDown which is conservative (favors protocol)
  • Handles the case where yield has already been claimed

137-154: LGTM: Robust claim implementation with DOS protection.

The claim function is well-implemented:

  • DOS protection: min(balanceOf(contract), yield(user)) prevents griefing when contract balance is insufficient to cover all owed yield
  • Clear error handling with NoClaimableYield revert
  • Correct event emission
  • No reentrancy concerns (state changes in _update, event after)

156-224: LGTM: Comprehensive and correct yield-bearing transfer logic.

The _update override is well-structured and handles all transfer scenarios correctly:

Flow correctness:

  • Calls super._update first to handle balance updates
  • Early returns for no-ops and yield payouts (no principal changes needed)
  • Blocks transfers TO the contract from non-zero addresses (prevents accidental loss)

Yield accrual (lines 189-201):

  • Correctly checks totalPrincipal != 0 before division (addresses past review concern)
  • Returns early without updating index when no principal exists (safe handling)
  • Uses getIndexRoundedDown which is conservative (favors protocol)

Minting (lines 203-210):

  • Rounds principal DOWN (protocol receives slightly more present value per principal unit)

Transfers & burns (lines 212-223):

  • Rounds principal UP when deducting (prevents users from transferring with insufficient principal)
  • Uses getSafePrincipalAmountRoundedUp with sender's principal as max bound

Rounding strategy: Consistently favors the protocol in all scenarios, preventing economic exploits while being fair to users.

contracts/test/NobleDollar.t.sol (5)

149-214: LGTM: Comprehensive revert test coverage.

The revert tests correctly validate:

  • InvalidTransfer when attempting to transfer or transferFrom to the contract from a non-zero address
  • NoClaimableYield when claiming with no available yield or zero balance

The use of abi.encodeWithSelector is correct for errors with parameters, and bare .selector for parameterless errors.


216-239: LGTM: Proper uint112 boundary testing.

This test correctly verifies:

  • Minting the maximum uint112 amount succeeds
  • Attempting to exceed uint112 max fails gracefully

This validates the contract's principal overflow protection.


241-274: LGTM: Excellent test of index-based minting.

This test validates that new deposits after yield accrual correctly receive proportional principal based on the current index:

  • After 100% yield, index = 2.0
  • New 1M deposit receives 500k principal (1M ÷ 2.0)

This is a critical property of the yield-bearing mechanism.


276-363: LGTM: Thorough yield claim testing.

Both claim tests comprehensively validate:

test_claimYield:

  • Correct yield calculation (100% yield scenario)
  • Event emission with correct parameters
  • Post-claim state: balance doubles, contract balance zeroes, principal unchanged, yield resets to 0

test_claimYieldMultipleUsers:

  • Proportional yield distribution (1M and 2M deposits each receive 100% yield)
  • Independent claims by multiple users
  • Correct state for both users after claiming

435-518: LGTM: Excellent edge case coverage.

test_indexUpdateWithZeroTotalPrincipal (lines 435-456):
Validates the fix from past reviews—when totalPrincipal=0, yield accrual:

  • Succeeds without reverting (no division by zero)
  • Leaves index unchanged at 1e12
  • Increases supply by yield amount
    This confirms the guard at line 192 of NobleDollar.sol works correctly.

test_indexUpdateAfterBurn (lines 458-518):
Complex scenario testing burn mechanics and subsequent yield:

  • After burn, principal is reduced appropriately
  • Remaining principal can still claim yield
  • Subsequent yield accruals calculate correct index (line 517: index=4e12 is correct for 2M supply on 500k principal)

These tests provide strong coverage of edge cases and the yield/principal accounting system.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (6)
contracts/test/NobleDollar.t.sol (2)

34-54: Hardcoded initialization address still unexplained

The proxy initialization uses a hardcoded address 0x9e6B1022bE9BBF5aFd152483DAD9b88911bC8611 without explanation. This was previously flagged and should either use the MAILBOX constant or be extracted into a well-named constant with documentation.


404-404: Inconsistent revert encoding

Line 404 uses abi.encode(NobleDollar.NoClaimableYield.selector) instead of abi.encodeWithSelector(NobleDollar.NoClaimableYield.selector), inconsistent with lines 206, 212 in the same file.

contracts/package.json (1)

10-12: Patched dependency concerns remain valid for version 9.0.15

The patch applied to @hyperlane-xyz/core@9.0.15 introduces the same critical modifications previously flagged for version 8.0.0, including the Address.isContract replacement, altered ownership initialization, and removed initial mint. These changes impact security, initialization behavior, and token supply logic.

contracts/patches/@hyperlane-xyz%2Fcore@9.0.15.patch (3)

15-21: Address.isContract replacement still present

The patch replaces OpenZeppelin's Address.isContract(_contract) with _contract.code.length > 0, which was previously flagged as a security concern. While functionally similar in Solidity ≥0.8.0, Address.isContract may include additional validations.


31-40: Ownership initialization pattern change

The patch modifies the initialization to call __Ownable_init(_owner) directly instead of __Ownable_init() followed by _transferOwnership(_owner). This assumes an overload exists in OwnableUpgradeable, which may not be standard.


47-66: Initial mint removed from HypERC20 initialization

The patch removes the _totalSupply parameter and the _mint(msg.sender, _totalSupply) call from the initialize function, fundamentally changing the token initialization behavior. No initial supply is minted, requiring all tokens to come from subsequent operations.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 225fdc1 and 2ac8c47.

⛔ Files ignored due to path filters (1)
  • contracts/bun.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • contracts/foundry.toml (1 hunks)
  • contracts/package.json (1 hunks)
  • contracts/patches/@hyperlane-xyz%2Fcore@9.0.15.patch (1 hunks)
  • contracts/src/HyperliquidNobleDollar.sol (1 hunks)
  • contracts/src/NobleDollar.sol (1 hunks)
  • contracts/test/NobleDollar.t.sol (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • contracts/foundry.toml
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: test (TestIBCYieldDistributionTimeout)

Comment on lines +42 to +48
/// @notice Claims all available yield for the bridge and transfers it to the owner.
function claimForBridge() public {
uint256 amount = claim(bridge);

_update(bridge, owner(), amount);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Missing access control on claimForBridge()

The claimForBridge() function is public with no access control, allowing anyone to trigger yield claims for the bridge and transfer those funds to the owner. This could be exploited to grief the bridge, manipulate yield timing, or interfere with bridge operations.

Apply this diff to restrict access:

-    function claimForBridge() public {
+    function claimForBridge() public onlyOwner {
         uint256 amount = claim(bridge);
         
         _update(bridge, owner(), amount);
     }

Alternatively, consider whether this function should exist at all, or if the bridge should manage its own yield claims.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// @notice Claims all available yield for the bridge and transfers it to the owner.
function claimForBridge() public {
uint256 amount = claim(bridge);
_update(bridge, owner(), amount);
}
}
/// @notice Claims all available yield for the bridge and transfers it to the owner.
function claimForBridge() public onlyOwner {
uint256 amount = claim(bridge);
_update(bridge, owner(), amount);
}
}
🤖 Prompt for AI Agents
In contracts/src/HyperliquidNobleDollar.sol around lines 42 to 48, the public
function claimForBridge() lacks access control and allows anyone to trigger
claims that transfer yield to the owner; restrict this by making the function
callable only by an authorized actor (e.g., onlyOwner or onlyBridge): add or
reuse an access-control modifier (import/inherit Ownable if not present or add a
bridge-only modifier), change the function visibility if needed (public ->
external) and apply the modifier to the function declaration, or remove the
function entirely if bridge should manage its own claims; ensure tests and
documentation are updated accordingly.

@keyleu keyleu self-requested a review November 17, 2025 13:56
keyleu
keyleu previously requested changes Nov 17, 2025
Copy link

@keyleu keyleu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One critical issue:
The bridge address is currently set in the constructor of HyperliquidNobleDollar.sol, but this contract is designed to be upgradable (deployed behind a proxy), leading to this value being always address(0) when accessed via the Proxy.

So we have two options:

  1. move the initialization to HyperliquidNobleDollar.sol and set the bridge address there.

  2. If we are 100% that the tokenId will never change, we can make the bridge address immutable and calculate it like now in the constructor. This works because immutable variables are part of the deployed bytecode

* apply fixes to make bridge upgradeable

* typo

* john's comments

* use vm.addr instead
Copy link

@keyleu keyleu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, one thing I just noticed rn is that the bridge yield claim is not documented in the README so it maybe worth to add it there? considering it's a permissionless action

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (3)
contracts/src/HyperliquidNobleDollar.sol (1)

46-51: Permissionless claimForBridge seems intentional; confirm this matches protocol expectations

claimForBridge() is public and lets any caller:

  1. Claim yield for bridge via the internal claim(bridge), and
  2. Transfer that claimed amount from bridge to owner() using _update.

Tests explicitly assert that “anyone can claim on behalf of the bridge” and that the yield always flows to the owner, so there’s no direct value-stealing risk. The main effect is that third parties can control when bridge yield is realized and, by design, reallocate part of the bridge principal to the owner.

If that’s the intended economics, the implementation is fine; otherwise, consider tightening this to onlyOwner or a dedicated bridge role.

contracts/test/NobleDollar.t.sol (2)

49-64: Clarify the magic hook address used in proxy initialization

initData hardcodes 0x9e6B1022bE9BBF5aFd152483DAD9b88911bC8611 as the hook argument to initialize. If this is intended to represent a specific mailbox/hook on mainnet, consider extracting it into a named constant (e.g., HYPERCORE_HOOK) or at least adding a short comment to describe what it is. That will make the test setup easier to understand and maintain alongside MAILBOX and TOKENID.


424-426: Fix revert expectation encoding for NoClaimableYield

Here you use vm.expectRevert(abi.encode(BaseNobleDollar.NoClaimableYield.selector));, which produces ABI-encoded data for a bytes4 value (32 bytes), while the contract reverts with just the 4‑byte selector for NoClaimableYield(). This mismatch will cause the test to fail or not actually verify the revert payload.

Align this with your other tests by either:

-        vm.expectRevert(abi.encode(BaseNobleDollar.NoClaimableYield.selector));
+        vm.expectRevert(BaseNobleDollar.NoClaimableYield.selector);

or:

-        vm.expectRevert(abi.encode(BaseNobleDollar.NoClaimableYield.selector));
+        vm.expectRevert(abi.encodeWithSelector(BaseNobleDollar.NoClaimableYield.selector));

Both are correct; the first is shorter and matches the pattern you already use in test_noClaimableYield.

🧹 Nitpick comments (1)
contracts/src/NobleDollar.sol (1)

88-101: Initialize / UUPS pattern looks sound; consider marking the override as initializer for clarity

The constructor disables initializers and the external initialize delegates to super.initialize, which should already be guarded by OZ-style initializer, so re-inits will revert. From a readability standpoint, you might still want to declare the override as initializer (or reinitializer if that’s how Hyperlane exposes it) to make the upgradeable intent explicit at this level, but functionally this is fine.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2ac8c47 and b2d5c7d.

⛔ Files ignored due to path filters (1)
  • contracts/bun.lock is excluded by !**/*.lock
📒 Files selected for processing (3)
  • contracts/src/HyperliquidNobleDollar.sol (1 hunks)
  • contracts/src/NobleDollar.sol (1 hunks)
  • contracts/test/NobleDollar.t.sol (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build
  • GitHub Check: prepare
🔇 Additional comments (6)
contracts/src/NobleDollar.sol (3)

133-141: Yield calculation is consistent with index-based accounting

yield(account) using getPresentAmountRoundedDown(principal, index) minus balanceOf(account) with a floor at zero matches the principal/index model and ensures you never report negative yield when users hold more than their “indexed” balance. This aligns with the way _update keeps principal in sync, so the view logic here looks correct.


158-169: Claim logic correctly caps by contract balance and avoids principal mutation

Using UIntMath.min256(balanceOf(address(this)), yield(user)) ensures you never overdraw the contract and that partial claims behave gracefully. Delegating the transfer to _update(address(this), user, amount) and relying on the from == address(this) early-return to skip principal updates preserves the accounting invariants. The custom NoClaimableYield error covers both “no yield” and “insufficient contract balance” in a single revert, which is a reasonable trade-off.


190-239: _update() branches cover all transfer modes and maintain principal/index invariants

The override correctly:

  • Calls super._update first to keep ERC20/bridge behavior intact.
  • Treats from == address(this) as a pure payout path (no principal changes).
  • Disallows arbitrary transfers to the contract while allowing mint-to-contract for yield accrual.
  • Updates the index only on to == address(this) mints, with a guard for totalPrincipal == 0 to avoid division by zero.
  • Mints principal on deposits (from == address(0) and to != address(this)) and uses a safe rounded-up principal burn/transfer path that’s capped by fromPrincipal.

Given the tests you’ve added around burns, second deposits, and multi-user yield, this looks coherent and robust.

If you haven’t already, it’s worth double-checking that IndexingMath.getSafePrincipalAmountRoundedUp always returns a value <= fromPrincipal in all edge cases (especially near uint112 limits), since _update relies on that guarantee to avoid underflow. A quick targeted test or invariant check around that helper would lock this in.

contracts/src/HyperliquidNobleDollar.sol (1)

33-44: Bridge address derivation and initialize flow look consistent

initialize(hook_, ism_, tokenId_) delegating to super.initialize(hook_, ism_) and then deriving bridge via 0x20 << 152 | tokenId_ aligns with the documented Hyperliquid pattern you’ve quoted, and the test in NobleDollar.t.sol asserting bridge() against a concrete address gives good coverage. No changes needed here.

contracts/test/NobleDollar.t.sol (2)

216-236: NoClaimableYield revert expectations are correct and consistent here

Using vm.expectRevert(BaseNobleDollar.NoClaimableYield.selector); leverages the bytes4 overload, which matches the revert data for a selector-only custom error and is consistent with the behavior of claim() when no yield is available. These negative-path checks for “no yield on fresh mint” and “no yield for zero-balance account” look good.


589-613: Upgrade test covers key invariants across implementations

test_Upgrade exercises:

  • Rejection of upgrades from a non-owner.
  • Successful upgrade from owner.
  • Preservation of critical state (bridge, index, totalSupply, totalPrincipal).
  • Ability to use newly-added state (bridge2) post-upgrade.

This is a solid sanity check for the UUPS flow and the EIP‑7201 storage layout assumptions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants