diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 120000 index 09eb304..0000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1 +0,0 @@ -/nix/store/m6bkmh5cm3qlgi9pgmxg3kb4kvpj6lkv-pre-commit-config.json \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..640c7d2 --- /dev/null +++ b/.pre-commit-config.yaml @@ -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 diff --git a/README.md b/README.md index c35d9c7..774ec66 100644 --- a/README.md +++ b/README.md @@ -8,18 +8,21 @@ Provides a `dia-price` word that fetches prices from the DIA oracle on-chain. ```rain using-words-from -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 @@ -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. diff --git a/meta/DiaSubParserAuthoringMeta.rain.meta b/meta/DiaSubParserAuthoringMeta.rain.meta index 9962457..29714cb 100644 Binary files a/meta/DiaSubParserAuthoringMeta.rain.meta and b/meta/DiaSubParserAuthoringMeta.rain.meta differ diff --git a/meta/DiaWords.rain.meta b/meta/DiaWords.rain.meta index 5ea7cc8..903cbd0 100644 Binary files a/meta/DiaWords.rain.meta and b/meta/DiaWords.rain.meta differ diff --git a/src/abstract/DiaExtern.sol b/src/abstract/DiaExtern.sol index b308f58..63694ca 100644 --- a/src/abstract/DiaExtern.sol +++ b/src/abstract/DiaExtern.sol @@ -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; @@ -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; diff --git a/src/abstract/DiaSubParser.sol b/src/abstract/DiaSubParser.sol index 7dcb73c..4c11293 100644 --- a/src/abstract/DiaSubParser.sol +++ b/src/abstract/DiaSubParser.sol @@ -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; @@ -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; @@ -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 + ); } } diff --git a/src/generated/DiaWords.pointers.sol b/src/generated/DiaWords.pointers.sol index 1c3038b..1dab462 100644 --- a/src/generated/DiaWords.pointers.sol +++ b/src/generated/DiaWords.pointers.sol @@ -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: diff --git a/src/lib/dia/LibDia.sol b/src/lib/dia/LibDia.sol index 1775ef8..d19e150 100644 --- a/src/lib/dia/LibDia.sol +++ b/src/lib/dia/LibDia.sol @@ -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); - /// @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; @@ -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 diff --git a/src/lib/parse/LibDiaSubParser.sol b/src/lib/parse/LibDiaSubParser.sol index 2e060e6..a1d92d8 100644 --- a/src/lib/parse/LibDiaSubParser.sol +++ b/src/lib/parse/LibDiaSubParser.sol @@ -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); } diff --git a/test/lib/LibFork.sol b/test/lib/LibFork.sol index 45b8f44..8d8293c 100644 --- a/test/lib/LibFork.sol +++ b/test/lib/LibFork.sol @@ -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; diff --git a/test/src/concrete/DiaWords.diaPrice.t.sol b/test/src/concrete/DiaWords.diaPrice.t.sol index f496559..da6455c 100644 --- a/test/src/concrete/DiaWords.diaPrice.t.sol +++ b/test/src/concrete/DiaWords.diaPrice.t.sol @@ -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"; @@ -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); diff --git a/test/src/lib/dia/LibDia.t.sol b/test/src/lib/dia/LibDia.t.sol index eaa891a..8f98ac6 100644 --- a/test/src/lib/dia/LibDia.t.sol +++ b/test/src/lib/dia/LibDia.t.sol @@ -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. @@ -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); + } } diff --git a/test/src/lib/op/LibOpDiaPrice.t.sol b/test/src/lib/op/LibOpDiaPrice.t.sol index f512dff..e06942b 100644 --- a/test/src/lib/op/LibOpDiaPrice.t.sol +++ b/test/src/lib/op/LibOpDiaPrice.t.sol @@ -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) { @@ -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);