Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .pre-commit-config.yaml

This file was deleted.

34 changes: 34 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Portable pre-commit config (no Nix store paths).
# Install: pip install pre-commit && pre-commit install
# Or with tools on PATH: pre-commit run --all-files

default_language_version:
python: python3

repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer

- repo: https://github.com/shellcheck-py/shellcheck-py
rev: v0.10.0.1
hooks:
- id: shellcheck

- repo: https://github.com/pre-commit/mirrors-prettier
rev: v3.1.0
hooks:
- id: prettier
types_or: [markdown, yaml, json]

- repo: local
hooks:
- id: forge-fmt
name: forge fmt
# Skip when forge is not on PATH (e.g. IDE Git); run via nix develop for enforcement.
entry: bash -c 'command -v forge >/dev/null 2>&1 || exit 0; exec forge fmt'
language: system
files: \.sol$
pass_filenames: false
28 changes: 23 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,21 @@ Provides a `dia-price` word that fetches prices from the DIA oracle on-chain.

```rain
using-words-from <DiaWords address>
price updated-at: dia-price("BTC/USD" 3600);
price updated-at: dia-price("AMZN" 3600);
```

### Inputs

1. **key** — DIA price feed key as a string, e.g. `"BTC/USD"`, `"ETH/USD"`. Passed through directly to the DIA oracle contract.
2. **staleAfter** — Maximum age of the price in seconds. Reverts if the price is older than this.
1. **key** — DIA price feed key as a string, e.g. `"AMZN"`, `"NVDA"`. Passed
through directly to the DIA oracle contract.
2. **staleAfter** — Maximum age of the price in seconds. Reverts if the price is
older than this.

### Outputs

1. **price** — The asset price as a Float (8 decimal places).
2. **updatedAt** — The timestamp of the last price update as a Float (unix seconds).
1. **price** — The asset price as a Float (18 decimal places).
2. **updatedAt** — The timestamp of the last price update as a Float (unix
seconds).

## Supported chains

Expand All @@ -37,3 +40,18 @@ forge test
# Regenerate pointers
forge script script/BuildPointers.sol
```

### Pre-commit

Git hooks use the committed `.pre-commit-config.yaml` (not Nix store symlinks).
Install once, then hooks run on `git commit`:

```sh
pip install pre-commit
pre-commit install
pre-commit run --all-files
```

Solidity formatting runs when `forge` is on `PATH` (e.g. inside `nix develop`);
otherwise the hook is skipped so IDE commits still work. Run
`nix develop -c forge fmt` before committing `.sol` changes if needed.
Binary file modified meta/DiaSubParserAuthoringMeta.rain.meta
Binary file not shown.
Binary file modified meta/DiaWords.rain.meta
Binary file not shown.
20 changes: 6 additions & 14 deletions src/abstract/DiaExtern.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,9 @@ abstract contract DiaExtern is BaseRainterpreterExternNPE2 {
}

function buildOpcodeFunctionPointers() external pure returns (bytes memory) {
function(OperandV2, StackItem[] memory)
internal
view
returns (StackItem[] memory)[] memory fs = new function(OperandV2, StackItem[] memory)
internal
view
returns (StackItem[] memory)[](OPCODE_FUNCTION_POINTERS_LENGTH);
function(OperandV2, StackItem[] memory) internal view returns (StackItem[] memory)[] memory fs = new function(OperandV2, StackItem[] memory)
internal
view returns (StackItem[] memory)[](OPCODE_FUNCTION_POINTERS_LENGTH);
fs[OPCODE_DIA_PRICE] = LibOpDiaPrice.run;

uint256[] memory pointers;
Expand All @@ -42,13 +38,9 @@ abstract contract DiaExtern is BaseRainterpreterExternNPE2 {
}

function buildIntegrityFunctionPointers() external pure returns (bytes memory) {
function(OperandV2, uint256, uint256)
internal
pure
returns (uint256, uint256)[] memory fs = new function(OperandV2, uint256, uint256)
internal
pure
returns (uint256, uint256)[](OPCODE_FUNCTION_POINTERS_LENGTH);
function(OperandV2, uint256, uint256) internal pure returns (uint256, uint256)[] memory fs = new function(OperandV2, uint256, uint256)
internal
pure returns (uint256, uint256)[](OPCODE_FUNCTION_POINTERS_LENGTH);
fs[OPCODE_DIA_PRICE] = LibOpDiaPrice.integrity;

uint256[] memory pointers;
Expand Down
23 changes: 9 additions & 14 deletions src/abstract/DiaSubParser.sol
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,8 @@ abstract contract DiaSubParser is BaseRainterpreterSubParserNPE2 {
}

function buildOperandHandlerFunctionPointers() external pure returns (bytes memory) {
function(bytes32[] memory) internal pure returns (OperandV2)[] memory fs = new function(bytes32[] memory)
internal
pure
returns (OperandV2)[](SUB_PARSER_WORD_PARSERS_LENGTH);
function(bytes32[] memory) internal pure returns (OperandV2)[] memory fs =
new function(bytes32[] memory) internal pure returns (OperandV2)[](SUB_PARSER_WORD_PARSERS_LENGTH);
fs[SUB_PARSER_WORD_DIA_PRICE] = LibParseOperand.handleOperandDisallowed;

uint256[] memory pointers;
Expand All @@ -57,13 +55,9 @@ abstract contract DiaSubParser is BaseRainterpreterSubParserNPE2 {
}

function buildSubParserWordParsers() external pure returns (bytes memory) {
function(uint256, uint256, OperandV2)
internal
view
returns (bool, bytes memory, bytes32[] memory)[] memory fs = new function(uint256, uint256, OperandV2)
internal
view
returns (bool, bytes memory, bytes32[] memory)[](SUB_PARSER_WORD_PARSERS_LENGTH);
function(uint256, uint256, OperandV2) internal view returns (bool, bytes memory, bytes32[] memory)[] memory fs = new function(uint256, uint256, OperandV2)
internal
view returns (bool, bytes memory, bytes32[] memory)[](SUB_PARSER_WORD_PARSERS_LENGTH);
fs[SUB_PARSER_WORD_DIA_PRICE] = diaPriceSubParser;

uint256[] memory pointers;
Expand All @@ -80,8 +74,9 @@ abstract contract DiaSubParser is BaseRainterpreterSubParserNPE2 {
returns (bool, bytes memory, bytes32[] memory)
{
// slither-disable-next-line unused-return
return LibSubParse.subParserExtern(
IInterpreterExternV4(extern()), constantsHeight, ioByte, operand, OPCODE_DIA_PRICE
);
return
LibSubParse.subParserExtern(
IInterpreterExternV4(extern()), constantsHeight, ioByte, operand, OPCODE_DIA_PRICE
);
}
}
4 changes: 2 additions & 2 deletions src/generated/DiaWords.pointers.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ pragma solidity ^0.8.25;
// file needs the contract to exist so that it can be compiled.

/// @dev Hash of the known bytecode.
bytes32 constant BYTECODE_HASH = bytes32(0x157692d69cb425a3e6159c50374491d20546d625178b37ca2737df8dd3d838dc);
bytes32 constant BYTECODE_HASH = bytes32(0x53e6e58f065ec84a6b45d4ebb6e7ff03d1e27325b4a4eca65e7e2c50647116de);

/// @dev The hash of the meta that describes the contract.
bytes32 constant DESCRIBED_BY_META_HASH = bytes32(0xc865c530891587d866bcf511c911ef3847c5b94d31a4ca9c8f0cbca940e86f2f);
bytes32 constant DESCRIBED_BY_META_HASH = bytes32(0xe5a735c20e9030c3d2727be05ed8f2f6121b9b6dcd252bb5d318d37498102881);

/// @dev The parse meta that is used to lookup word definitions.
/// The structure of the parse meta is:
Expand Down
15 changes: 7 additions & 8 deletions src/lib/dia/LibDia.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,17 @@ error ZeroDiaPrice(string key);

/// @title LibDia
/// @notice Core library for interacting with DIA oracle V2 on-chain.
/// DIA keys are simple strings like "BTC/USD", "ETH/USD", etc.
/// DIA keys are ticker strings like "AMZN", "NVDA", etc.
/// The string is passed through directly from the Rain expression.
/// DIA prices have 8 decimals.
/// DIA prices have 18 decimals.
library LibDia {
uint256 constant CHAIN_ID_BASE = 8453;

/// @dev DIA oracle V2 contract on Base.
/// https://docs.diadata.org/products/token-price-feeds/access-the-oracle
IDIAOracleV2 constant ORACLE_BASE = IDIAOracleV2(0xB8BF9ba432282F25F56e143641145349ab7c5Bf6);
IDIAOracleV2 constant ORACLE_BASE = IDIAOracleV2(0xCE521b52513242c5094bc56f57887BB2A05B8129);
Comment thread
Siddharth2207 marked this conversation as resolved.

/// @dev DIA prices have 8 decimal places.
int256 constant DIA_DECIMALS = -8;
/// @dev DIA prices have 18 decimal places.
int256 constant DIA_DECIMALS = -18;

/// @dev Mask for the 5 bit length from V3 IntOrAString.
uint256 constant LENGTH_MASK_V3 = 0x1f;
Expand Down Expand Up @@ -58,10 +57,10 @@ library LibDia {

/// @notice Fetches a price from the DIA oracle and reverts if the price is
/// stale or zero. The key is passed through as a string directly from the
/// Rain expression, e.g. "BTC/USD".
/// Rain expression, e.g. "AMZN".
/// @param feedKey The V3 IntOrAString key for the DIA feed.
/// @param staleAfter The maximum age of the price in seconds as a Float.
/// @return price The price as a Float with 8 decimal places.
/// @return price The price as a Float with 18 decimal places.
/// @return updatedAt The timestamp of the price update as a Float (seconds).
function getPriceNoOlderThan(IntOrAString feedKey, Float staleAfter)
internal
Expand Down
2 changes: 1 addition & 1 deletion src/lib/parse/LibDiaSubParser.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ library LibDiaSubParser {

meta[SUB_PARSER_WORD_DIA_PRICE] = AuthoringMetaV2(
"dia-price",
"Returns the current price of the given asset according to DIA. Accepts 2 inputs, the price key as a string (e.g. \"BTC/USD\") and the timeout in seconds. The price has 8 decimal places. The timeout will be used to determine if the price is stale and revert if it is. Returns 2 outputs: the price and the timestamp of the last update."
"Returns the current price of the given asset according to DIA. Accepts 2 inputs, the price key as a string (e.g. \"AMZN\") and the timeout in seconds. The price has 18 decimal places. The timeout will be used to determine if the price is stale and revert if it is. Returns 2 outputs: the price and the timestamp of the last update."
);
return abi.encode(meta);
}
Expand Down
12 changes: 4 additions & 8 deletions test/lib/LibFork.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,7 @@ pragma solidity ^0.8.25;

string constant FORK_RPC_URL_BASE = "https://base.gateway.tenderly.co";

/// @dev A recent Base block. DIA demo oracle has BTC/USD data with timestamp
/// 1744172776 (April 9, 2025). Tests use vm.warp to set block.timestamp
/// close to the DIA update time so staleness checks pass.
uint256 constant FORK_BLOCK_BASE = 44515230;

/// @dev The timestamp of the DIA BTC/USD update at the demo oracle.
/// Tests warp to this + a small offset so staleness checks pass.
uint256 constant DIA_BTC_USD_TIMESTAMP = 1744172776;
/// @dev A recent Base block. DIA oracle has AMZN data with timestamp
/// 1778908932. The fork block timestamp is close enough to that update for
/// staleness checks without vm.warp.
uint256 constant FORK_BLOCK_BASE = 46061133;
5 changes: 2 additions & 3 deletions test/src/concrete/DiaWords.diaPrice.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pragma solidity =0.8.25;

import {Test} from "forge-std/Test.sol";
import {DiaWords} from "src/concrete/DiaWords.sol";
import {FORK_RPC_URL_BASE, FORK_BLOCK_BASE, DIA_BTC_USD_TIMESTAMP} from "test/lib/LibFork.sol";
import {FORK_RPC_URL_BASE, FORK_BLOCK_BASE} from "test/lib/LibFork.sol";
import {LibDia} from "src/lib/dia/LibDia.sol";
import {LibDecimalFloat, Float} from "rain.math.float/lib/LibDecimalFloat.sol";
import {IntOrAString} from "rain.intorastring/lib/LibIntOrAString.sol";
Expand All @@ -28,12 +28,11 @@ contract DiaWordsDiaPriceTest is Test {
function testDiaWordsExternDispatch() external {
vm.createSelectFork(FORK_RPC_URL_BASE, FORK_BLOCK_BASE);
vm.chainId(LibDia.CHAIN_ID_BASE);
vm.warp(DIA_BTC_USD_TIMESTAMP + 60);

DiaWords diaWords = new DiaWords();

StackItem[] memory inputs = new StackItem[](2);
inputs[0] = StackItem.wrap(bytes32(IntOrAString.unwrap(fromStringV3("BTC/USD"))));
inputs[0] = StackItem.wrap(bytes32(IntOrAString.unwrap(fromStringV3("AMZN"))));
inputs[1] = StackItem.wrap(Float.unwrap(LibDecimalFloat.packLossless(3600, 0)));

StackItem[] memory outputs = LibOpDiaPrice.run(OperandV2.wrap(0), inputs);
Expand Down
57 changes: 45 additions & 12 deletions test/src/lib/dia/LibDia.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {Test} from "forge-std/Test.sol";
import {LibDia, IDIAOracleV2, UnsupportedChainId} from "src/lib/dia/LibDia.sol";
import {IntOrAString} from "rain.intorastring/lib/LibIntOrAString.sol";
import {Float, LibDecimalFloat} from "rain.math.float/lib/LibDecimalFloat.sol";
import {FORK_RPC_URL_BASE, FORK_BLOCK_BASE, DIA_BTC_USD_TIMESTAMP} from "test/lib/LibFork.sol";
import {FORK_RPC_URL_BASE, FORK_BLOCK_BASE} from "test/lib/LibFork.sol";

/// @dev Create a V3-encoded IntOrAString matching the latest Rain parser output.
/// Layout: string data right-aligned above the low byte, low byte = 0xE0 | length.
Expand Down Expand Up @@ -40,36 +40,69 @@ contract LibDiaGetOracleContractTest is Test {

contract LibDiaStringV3Test is Test {
function testRoundTrip() external pure {
IntOrAString encoded = fromStringV3("BTC/USD");
IntOrAString encoded = fromStringV3("AMZN");
string memory decoded = LibDia.intOrAStringToString(encoded);
assertEq(decoded, "BTC/USD");
assertEq(decoded, "AMZN");
}

function testRoundTripETH() external pure {
IntOrAString encoded = fromStringV3("ETH/USD");
function testRoundTripNVDA() external pure {
IntOrAString encoded = fromStringV3("NVDA");
string memory decoded = LibDia.intOrAStringToString(encoded);
assertEq(decoded, "ETH/USD");
assertEq(decoded, "NVDA");
}
}

/// @dev Hardcoded prices are pinned to a Base fork snapshot (see comments on each test).
/// Oracle: 0xCE521b52513242c5094bc56f57887BB2A05B8129 (LibDia.ORACLE_BASE).
/// Fork block: 46061133 (block.timestamp 1778911613, 2026-05-16 06:06:53 UTC).
/// Reproduce: cast call 0xCE521b52513242c5094bc56f57887BB2A05B8129
/// "getValue(string)(uint128,uint128)" SYMBOL --rpc-url https://mainnet.base.org --block 46061133
/// When changing FORK_BLOCK_BASE in test/lib/LibFork.sol, re-run that command per symbol and
/// update the raw uint128 price literals below (first return value; 18 decimals).
contract LibDiaGetPriceTest is Test {
function testGetPriceBtcUsd() external {
function setUp() external {
vm.createSelectFork(FORK_RPC_URL_BASE, FORK_BLOCK_BASE);
vm.chainId(8453);
vm.warp(DIA_BTC_USD_TIMESTAMP + 60);
}

IntOrAString key = fromStringV3("BTC/USD");
/// @dev Asserts getValue(key) raw price at the fork block matches `rawPrice` (18-decimal uint128).
function _assertFeedPrice(string memory symbol, uint256 rawPrice) internal view {
IntOrAString key = fromStringV3(symbol);
Float staleAfter = LibDecimalFloat.packLossless(3600, 0);

(Float price, Float updatedAt) = LibDia.getPriceNoOlderThan(key, staleAfter);

assertTrue(Float.unwrap(price) != 0, "price should be non-zero");
assertTrue(Float.unwrap(updatedAt) != 0, "timestamp should be non-zero");

assertEq(
Float.unwrap(price),
Float.unwrap(LibDecimalFloat.packLossless(int256(uint256(7568457939217)), -8)),
"unexpected BTC price"
Float.unwrap(LibDecimalFloat.packLossless(int256(rawPrice), -18)),
string.concat("unexpected ", symbol, " price")
);
}

/// raw price 264100000000000022736 (~$264.10); update timestamp 1778908932 at fork block.
function testGetPriceAmzn() external {
_assertFeedPrice("AMZN", 264100000000000022736);
}

/// raw price 225389999999999986352 (~$225.39); update timestamp 1778908933 at fork block.
function testGetPriceNvda() external {
_assertFeedPrice("NVDA", 225389999999999986352);
}

/// raw price 195539999999999992048 (~$195.54); update timestamp 1778908934 at fork block.
function testGetPriceCoin() external {
_assertFeedPrice("COIN", 195539999999999992048);
}

/// raw price 177455000000000012512 (~$177.46); update timestamp 1778908935 at fork block.
function testGetPriceMstr() external {
_assertFeedPrice("MSTR", 177455000000000012512);
}

/// raw price 422350000000000022752 (~$422.35); update timestamp 1778908936 at fork block.
function testGetPriceTsla() external {
_assertFeedPrice("TSLA", 422350000000000022752);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
5 changes: 2 additions & 3 deletions test/src/lib/op/LibOpDiaPrice.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ pragma solidity =0.8.25;
import {Test} from "forge-std/Test.sol";
import {LibOpDiaPrice, OperandV2, StackItem} from "src/lib/op/LibOpDiaPrice.sol";
import {IntOrAString} from "rain.intorastring/lib/LibIntOrAString.sol";
import {FORK_RPC_URL_BASE, FORK_BLOCK_BASE, DIA_BTC_USD_TIMESTAMP} from "test/lib/LibFork.sol";
import {FORK_RPC_URL_BASE, FORK_BLOCK_BASE} from "test/lib/LibFork.sol";
import {Float, LibDecimalFloat} from "rain.math.float/lib/LibDecimalFloat.sol";

function fromStringV3(string memory s) pure returns (IntOrAString intOrAString) {
Expand All @@ -27,10 +27,9 @@ contract LibOpDiaPriceTest is Test {
function testRunForkCurrentPriceHappy() external {
vm.createSelectFork(FORK_RPC_URL_BASE, FORK_BLOCK_BASE);
vm.chainId(8453);
vm.warp(DIA_BTC_USD_TIMESTAMP + 60);

StackItem[] memory inputs = new StackItem[](2);
inputs[0] = StackItem.wrap(bytes32(IntOrAString.unwrap(fromStringV3("BTC/USD"))));
inputs[0] = StackItem.wrap(bytes32(IntOrAString.unwrap(fromStringV3("AMZN"))));
inputs[1] = StackItem.wrap(Float.unwrap(LibDecimalFloat.packLossless(3600, 0)));

StackItem[] memory outputs = LibOpDiaPrice.run(OperandV2.wrap(0), inputs);
Expand Down
Loading