Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(evm-oracle): add Solidity contract that we can use to expose the Nibiru Oracle in the ChainLink interface #2149

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

Unique-Divine
Copy link
Member

@Unique-Divine Unique-Divine commented Jan 8, 2025

Purpose / Abstract

[STILL DRAFT: Will add more context]

Summary by CodeRabbit

  • New Features

    • Added Nibiru Oracle integration with ChainLink interface
    • Published precompiled contracts and ABIs under @nibiruchain/solidity npm package
    • Introduced new Solidity contracts for Oracle and token interactions
  • Documentation

    • Updated README with package details and installation instructions
    • Added comprehensive CHANGELOG entry
  • Dependencies

    • Added @nibiruchain/solidity package (version 0.0.2)

Copy link
Contributor

coderabbitai bot commented Jan 8, 2025

Walkthrough

This pull request introduces a comprehensive enhancement to the Nibiru Oracle system by adding Solidity contracts and ABIs for ChainLink-like interactions. The changes include creating a new @nibiruchain/solidity npm package, implementing a NibiruOracleChainLinkLike contract, and extending the existing Oracle precompile with a new chainLinkLatestRoundData method. The implementation provides developers with improved access to oracle data, following standard Ethereum interface conventions.

Changes

File Change Summary
CHANGELOG.md Added entry for new Solidity contract and npm package
evm-e2e/package.json Added @nibiruchain/solidity as a dev dependency
justfile Added command to generate ABI JSON files
x/evm/embeds/README.md Updated branding and installation instructions
x/evm/embeds/contracts/IOracle.sol Added chainLinkLatestRoundData method and ChainLinkAggregatorV3Interface
x/evm/embeds/contracts/NibiruOracleChainLinkLike.sol New contract implementing ChainLink-like oracle interface
x/evm/precompile/oracle.go Added chainLinkLatestRoundData method to precompile

Sequence Diagram

sequenceDiagram
    participant Client
    participant NibiruOracleChainLinkLike
    participant OraclePrecompile
    participant OracleKeeper

    Client->>NibiruOracleChainLinkLike: latestRoundData()
    NibiruOracleChainLinkLike->>OraclePrecompile: chainLinkLatestRoundData(pair)
    OraclePrecompile->>OracleKeeper: Query exchange rate
    OracleKeeper-->>OraclePrecompile: Return rate and metadata
    OraclePrecompile-->>NibiruOracleChainLinkLike: Return round data
    NibiruOracleChainLinkLike-->>Client: Return roundId, answer, timestamps
Loading

Assessment against linked issues

Objective Addressed Explanation
Expose timestamp information for oracle exchange rate [#2075]
Implement ChainLink interface with 8 decimals [#2075]
Publish Solidity interfaces for precompiled contracts [#376]

Poem

🐰 Hop, hop, hooray! Oracle's new way,
ChainLink's embrace, data's bright display,
Timestamps dance, decimals align,
Nibiru's magic in each design,
Smart contracts sing, blockchain's delight! 🌟

Finishing Touches

  • 📝 Generate Docstrings

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

codecov bot commented Jan 8, 2025

Codecov Report

Attention: Patch coverage is 65.38462% with 9 lines in your changes missing coverage. Please review.

Project coverage is 65.21%. Comparing base (8e477cb) to head (c3e38bd).

Files with missing lines Patch % Lines
x/evm/precompile/oracle.go 65.38% 6 Missing and 3 partials ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main    #2149   +/-   ##
=======================================
  Coverage   65.21%   65.21%           
=======================================
  Files         277      277           
  Lines       22208    22234   +26     
=======================================
+ Hits        14482    14499   +17     
- Misses       6736     6742    +6     
- Partials      990      993    +3     
Files with missing lines Coverage Δ
x/evm/precompile/oracle.go 57.95% <65.38%> (+3.11%) ⬆️

@Unique-Divine Unique-Divine marked this pull request as ready for review January 9, 2025 09:03
@Unique-Divine Unique-Divine requested a review from a team as a code owner January 9, 2025 09:03
Copy link
Contributor

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

🧹 Nitpick comments (8)
x/evm/embeds/gen-abi/main.go (2)

17-26: Consider using go-git for repository root detection.

The current implementation using exec.Command is fragile and may fail in certain environments. Consider using go-git to reliably detect the repository root:

-func findRootPath() (string, error) {
-	rootPathBz, err := exec.Command("go", "list", "-m", "-f", "{{.Dir}}").Output()
-	if err != nil {
-		return "", err
-	}
-	rootPath := strings.Trim(string(rootPathBz), "\n")
-	return rootPath, nil
-}
+import "github.com/go-git/go-git/v5"
+
+func findRootPath() (string, error) {
+	wd, err := os.Getwd()
+	if err != nil {
+		return "", err
+	}
+	r, err := git.PlainOpenWithOptions(wd, &git.PlainOpenOptions{
+		DetectDotGit: true,
+	})
+	if err != nil {
+		return "", err
+	}
+	wt, err := r.Worktree()
+	if err != nil {
+		return "", err
+	}
+	return wt.Filesystem.Root(), nil
+}

49-53: Add error handling for malformed filenames.

The file filtering logic should handle potential edge cases with malformed filenames.

-		if !info.IsDir() &&
-			filepath.Ext(path) == ".json" && // .json files that
-			!strings.Contains(path, ".dbg") && // are NOT "dbg" files
-			!strings.HasPrefix(info.Name(), "Test") { // are NOT for tests
+		filename := info.Name()
+		if info.IsDir() || 
+			!strings.HasSuffix(strings.ToLower(filename), ".json") ||
+			strings.Contains(filename, ".dbg") ||
+			strings.HasPrefix(filename, "Test") {
+			return nil
+		}
x/evm/precompile/oracle.go (1)

178-178: Consider handling potential timestamp overflow.

The division of BlockTimestampMs by 1000 could potentially lose precision. Consider using a safer conversion method.

-	timestampSeconds := big.NewInt(priceAtBlock.BlockTimestampMs / 1000)
+	timestampSeconds := new(big.Int).Div(
+		new(big.Int).SetUint64(priceAtBlock.BlockTimestampMs),
+		big.NewInt(1000),
+	)
x/evm/embeds/package.json (1)

4-4: Consider semantic versioning for the initial release.

Since this is a new package with breaking changes from the previous implementation, consider starting with version 1.0.0 instead of 0.0.2.

-  "version": "0.0.2",
+  "version": "1.0.0",
evm-e2e/package.json (1)

30-30: Pin the package version for stability

Since @nibiruchain/solidity is in early development (v0.0.2), using ^ could allow breaking changes in minor versions. Consider pinning the exact version:

-    "@nibiruchain/solidity": "^0.0.2",
+    "@nibiruchain/solidity": "0.0.2",
x/evm/embeds/abi/IOracle.json (2)

3-9: Consider using bytes32 for pair parameter

Using string for the pair parameter is gas-intensive. ChainLink typically uses bytes32 for asset pair identifiers. Consider:

-        "internalType": "string",
-        "name": "pair",
-        "type": "string"
+        "internalType": "bytes32",
+        "name": "pair",
+        "type": "bytes32"

1-70: Consider adding events for price updates

The interface could benefit from events to allow monitoring of price updates. Consider adding:

event PriceUpdated(
    bytes32 indexed pair,
    uint256 price,
    uint256 timestamp
);
x/evm/embeds/README.md (1)

1-14: Enhance documentation with usage examples and API details

Consider adding:

  1. Usage examples showing how to interact with the Oracle
  2. API documentation for each function
  3. Version compatibility information (Solidity, ChainLink, etc.)

Example structure:

## Usage

```solidity
// Import the contract
import "@nibiruchain/solidity/contracts/NibiruOracleChainLinkLike.sol";

// Initialize
NibiruOracleChainLinkLike oracle = new NibiruOracleChainLinkLike("NIBI/USD");

// Get latest price
(
    uint80 roundId,
    int256 answer,
    uint256 startedAt,
    uint256 updatedAt,
    uint80 answeredInRound
) = oracle.latestRoundData();

API Reference

chainLinkLatestRoundData

Returns the latest round data in ChainLink format...


</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

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

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 8e477cb03fd9b2853456d0e7775aa157165dce0e and c3e38bd330a757c6e7ac4e9fe3d4c9aae4c41636.

</details>

<details>
<summary>⛔ Files ignored due to path filters (2)</summary>

* `evm-e2e/package-lock.json` is excluded by `!**/package-lock.json`
* `x/evm/embeds/package-lock.json` is excluded by `!**/package-lock.json`

</details>

<details>
<summary>📒 Files selected for processing (20)</summary>

* `CHANGELOG.md` (1 hunks)
* `evm-e2e/package.json` (1 hunks)
* `justfile` (1 hunks)
* `x/evm/embeds/README.md` (1 hunks)
* `x/evm/embeds/abi/ChainLinkAggregatorV3Interface.json` (1 hunks)
* `x/evm/embeds/abi/ERC20Minter.json` (1 hunks)
* `x/evm/embeds/abi/IFunToken.json` (1 hunks)
* `x/evm/embeds/abi/INibiruEvm.json` (1 hunks)
* `x/evm/embeds/abi/IOracle.json` (1 hunks)
* `x/evm/embeds/abi/IWasm.json` (1 hunks)
* `x/evm/embeds/abi/NibiruOracleChainLinkLike.json` (1 hunks)
* `x/evm/embeds/artifacts/contracts/IOracle.sol/ChainLinkAggregatorV3Interface.json` (1 hunks)
* `x/evm/embeds/artifacts/contracts/IOracle.sol/IOracle.json` (1 hunks)
* `x/evm/embeds/artifacts/contracts/NibiruOracleChainLinkLike.sol/NibiruOracleChainLinkLike.json` (1 hunks)
* `x/evm/embeds/contracts/IOracle.sol` (1 hunks)
* `x/evm/embeds/contracts/NibiruOracleChainLinkLike.sol` (1 hunks)
* `x/evm/embeds/gen-abi/main.go` (1 hunks)
* `x/evm/embeds/package.json` (1 hunks)
* `x/evm/precompile/oracle.go` (5 hunks)
* `x/evm/precompile/oracle_test.go` (1 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (2)</summary>

* x/evm/embeds/artifacts/contracts/IOracle.sol/ChainLinkAggregatorV3Interface.json
* x/evm/embeds/artifacts/contracts/NibiruOracleChainLinkLike.sol/NibiruOracleChainLinkLike.json

</details>

<details>
<summary>👮 Files not reviewed due to content moderation or server errors (5)</summary>

* x/evm/embeds/abi/NibiruOracleChainLinkLike.json
* x/evm/embeds/abi/IWasm.json
* x/evm/embeds/abi/IFunToken.json
* x/evm/embeds/abi/ERC20Minter.json
* CHANGELOG.md

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: integration-tests

</details>

<details>
<summary>🔇 Additional comments (13)</summary><blockquote>

<details>
<summary>x/evm/embeds/contracts/IOracle.sol (3)</summary>

`21-32`: **LGTM! Function signature matches ChainLink's interface.**

The new `chainLinkLatestRoundData` function correctly implements the ChainLink interface signature, enabling compatibility with existing ChainLink consumers.

---

`39-72`: **LGTM! Interface matches ChainLink's standard.**

The `ChainLinkAggregatorV3Interface` is correctly implemented with proper documentation and source attribution.

---

`37-37`: **Verify impact of constant rename.**

The constant has been renamed from `ORACLE_GATEWAY` to `NIBIRU_ORACLE`. While the new name better reflects its purpose, this is a breaking change.

</details>
<details>
<summary>justfile (1)</summary>

`42-43`: **LGTM! Build step properly integrated.**

The ABI generation step is correctly integrated into the existing build process.

</details>
<details>
<summary>x/evm/precompile/oracle.go (3)</summary>

`36-37`: **LGTM! Clear method naming for ChainLink compatibility.**

The new constant `OracleMethod_chainLinkLatestRoundData` follows the existing naming convention and clearly indicates its purpose.

---

`56-59`: **LGTM! Clean integration of ChainLink interface.**

The switch case addition is well-documented and properly integrated into the existing flow.

---

`176-179`: _:warning: Potential issue_

**Verify the `answeredInRound` implementation.**

The hardcoded value `420` for `answeredInRound` doesn't follow ChainLink's specification. According to ChainLink's documentation, this should represent the round ID in which the answer was computed.


Apply this diff to fix the implementation:

```diff
-	answeredInRound := big.NewInt(420) // for no reason in particular / unused
+	answeredInRound := roundId // should be the same as roundId per ChainLink's spec
x/evm/precompile/oracle_test.go (2)

142-146: LGTM! Thorough verification of exchange rate conversion.

The test properly verifies the 18 decimal precision required by ChainLink's interface.


147-148: LGTM! Proper timestamp verification.

The test correctly verifies both startedAt and updatedAt timestamps.

x/evm/embeds/abi/INibiruEvm.json (1)

1-21: LGTM! Well-structured event definition.

The event definition follows Solidity best practices with appropriate parameter indexing for efficient event filtering.

x/evm/embeds/package.json (1)

7-9: LGTM! Appropriate file patterns for package distribution.

The file patterns correctly include the necessary Solidity contracts and ABIs while excluding test files.

x/evm/embeds/artifacts/contracts/IOracle.sol/IOracle.json (1)

Line range hint 1-44: Implementation looks correct

The contract artifact correctly defines the interface with empty bytecode as expected.

x/evm/embeds/abi/ChainLinkAggregatorV3Interface.json (1)

1-113: Add standard error definitions

Consider adding standard ChainLink error definitions for consistency:

error StalePrice(uint256 updatedAt);
error InvalidRoundId(uint80 roundId);

Also verify the implementation handles these cases:

  1. Stale prices (when updatedAt is too old)
  2. Invalid round IDs
  3. Zero or negative prices

Comment on lines +23 to +27
constructor(string memory _pair, uint8 _dec) {
require(_dec <= 18, "Decimals cannot exceed 18");
pair = _pair;
_decimals = _dec;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add input validation for empty pair string.

The constructor should validate that the _pair parameter is not empty.

 constructor(string memory _pair, uint8 _dec) {
     require(_dec <= 18, "Decimals cannot exceed 18");
+    require(bytes(_pair).length > 0, "Pair string cannot be empty");
     pair = _pair;
     _decimals = _dec;
 }
📝 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
constructor(string memory _pair, uint8 _dec) {
require(_dec <= 18, "Decimals cannot exceed 18");
pair = _pair;
_decimals = _dec;
}
constructor(string memory _pair, uint8 _dec) {
require(_dec <= 18, "Decimals cannot exceed 18");
require(bytes(_pair).length > 0, "Pair string cannot be empty");
pair = _pair;
_decimals = _dec;
}

Comment on lines +104 to +111
function scaleAnswerToDecimals(
int256 answer18Dec
) internal view returns (int256 answer) {
// Default answers are in 18 decimals.
// Scale down to the decimals specified in the constructor.
uint8 pow10 = 18 - _decimals;
return answer18Dec / int256(10 ** pow10);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Prevent potential overflow in decimal scaling.

The decimal scaling operation should check for potential overflow conditions.

 function scaleAnswerToDecimals(
     int256 answer18Dec
 ) internal view returns (int256 answer) {
     // Default answers are in 18 decimals.
     // Scale down to the decimals specified in the constructor.
     uint8 pow10 = 18 - _decimals;
+    require(
+        answer18Dec > type(int256).min / (10 ** pow10) &&
+        answer18Dec < type(int256).max / (10 ** pow10),
+        "Decimal scaling would overflow"
+    );
     return answer18Dec / int256(10 ** pow10);
 }
📝 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
function scaleAnswerToDecimals(
int256 answer18Dec
) internal view returns (int256 answer) {
// Default answers are in 18 decimals.
// Scale down to the decimals specified in the constructor.
uint8 pow10 = 18 - _decimals;
return answer18Dec / int256(10 ** pow10);
}
function scaleAnswerToDecimals(
int256 answer18Dec
) internal view returns (int256 answer) {
// Default answers are in 18 decimals.
// Scale down to the decimals specified in the constructor.
uint8 pow10 = 18 - _decimals;
require(
answer18Dec > type(int256).min / (10 ** pow10) &&
answer18Dec < type(int256).max / (10 ** pow10),
"Decimal scaling would overflow"
);
return answer18Dec / int256(10 ** pow10);
}

Comment on lines +88 to +102
function getRoundData(
uint80
)
external
view
returns (
uint80 roundId,
int256 answer,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
)
{
return latestRoundData();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Document unsupported historical data retrieval.

The getRoundData function should revert when attempting to retrieve historical data instead of silently returning the latest data.

 function getRoundData(
-    uint80
+    uint80 _roundId
 )
     external
     view
     returns (
         uint80 roundId,
         int256 answer,
         uint256 startedAt,
         uint256 updatedAt,
         uint80 answeredInRound
     )
 {
+    (roundId, answer, startedAt, updatedAt, answeredInRound) = latestRoundData();
+    require(
+        _roundId == roundId,
+        "Historical data retrieval not supported"
+    );
-    return latestRoundData();
     return (roundId, answer, startedAt, updatedAt, answeredInRound);
 }
📝 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
function getRoundData(
uint80
)
external
view
returns (
uint80 roundId,
int256 answer,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
)
{
return latestRoundData();
}
function getRoundData(
uint80 _roundId
)
external
view
returns (
uint80 roundId,
int256 answer,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
)
{
(roundId, answer, startedAt, updatedAt, answeredInRound) = latestRoundData();
require(
_roundId == roundId,
"Historical data retrieval not supported"
);
return (roundId, answer, startedAt, updatedAt, answeredInRound);
}

Comment on lines +149 to +150
// answeredInRound
s.Equal(out[4].(*big.Int), big.NewInt(420))
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Update test to match the corrected implementation.

The test expects answeredInRound to be 420, but this should match the roundId value per ChainLink's specification.

Apply this diff to fix the test:

-		s.Equal(out[4].(*big.Int), big.NewInt(420))
+		s.Equal(out[4].(*big.Int), out[0].(*big.Int)) // answeredInRound should match roundId
📝 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
// answeredInRound
s.Equal(out[4].(*big.Int), big.NewInt(420))
// answeredInRound
s.Equal(out[4].(*big.Int), out[0].(*big.Int)) // answeredInRound should match roundId

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.

[evm] Oracle precompile UX would be improved by update time information [evm] publish reminder for Solidity
1 participant