BW-154: SimpleOracle signed price store and IPriceOracle adapter - #93
Merged
Conversation
CopyOracle stores the latest price per feed, accepting only readings carrying an EIP-712 signature from the configured signer. The domain binds each signature to the chain and the contract. Anyone may relay. CopyOraclePriceOracle adapts a CopyOracle feed to IPriceOracle with a max-age check and 8 -> 18 decimal scaling, mirroring the Pyth and HyperCore adapters. DeployCopyOracle deploys the store and one adapter per collateral in the chain's address book, points the GeneralManager at them, and records copyOracleAddress and the new priceOracles in the address book.
The signature is verified before the timestamp, so a bad signature always reverts InvalidSignature. Resubmitting the stored timestamp returns without writing or emitting; older and future timestamps still revert InvalidTimestamp.
|
CopyOracle named the off-chain service rather than the contract. The service copies Chainlink feeds; the contract is a simple store of signed prices and knows nothing about where they came from. The service in the monorepo keeps its name. The EIP-712 domain name changes with it, from "CopyOracle" to "SimpleOracle", so the digest changes and the off-chain signer moves in lockstep. The version string and the PriceUpdate typehash are unchanged. Nothing is deployed yet, so no signature in flight is affected. Also renames COPY_ORACLE_ADMIN and COPY_ORACLE_SIGNER to SIMPLE_ORACLE_ADMIN and SIMPLE_ORACLE_SIGNER, and the address-book key the deploy script writes from copyOracleAddress to simpleOracleAddress. Behaviour is otherwise untouched.
A public state variable named testAddressesFileSuffix gives Solidity an
auto-generated testAddressesFileSuffix() getter, and forge collects any
function whose name begins with "test". Every script inheriting DeployAll
was therefore run as a test suite, and BaseScript.setUp reads
DEPLOYER_ADDRESS through vm.envAddress, which reverts when the variable
is absent.
That only passed because test/script/DeployAll.t.sol calls
vm.setEnv("DEPLOYER_ADDRESS", ...), which mutates the environment for the
whole run: the script suite succeeded when that test happened to run
first and failed when it did not. Suite order is not guaranteed, so the
outcome depended on how many suites were present.
Renaming the variable and its setter drops the accidental getter, so no
script is collected and nothing depends on another suite's side effect.
SocksNFlops
approved these changes
Sep 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Changes
src/interfaces/ISimpleOracle.sol: interface for the signed price store (decimals,signer,setSigner,latestRoundData,updatePrice,updatePrices,PRICE_UPDATE_TYPEHASH, plus thePriceUpdated/SignerUpdatedevents andInvalidSignature/InvalidTimestamp/InvalidPrice/UnknownFeederrors).src/SimpleOracle.sol: the store. OpenZeppelinAccessControl(DEFAULT_ADMIN_ROLEto the constructor admin) andEIP712("SimpleOracle", "1").updatePriceis permissionless and checks, in order,price > 0,timestamp <= block.timestamp && timestamp > stored, then that the EIP-712 signature overPriceUpdate(bytes32 id,int256 price,uint256 timestamp)recovers tosigner.updatePrices(bytes[])decodes each element asabi.encode(id, price, timestamp, signature)and applies the same logic.latestRoundDatarevertsUnknownFeedfor a feed never set; the store does not enforce staleness.decimals()is 8 and prices are stored as signed.setSigneris admin-only and rejects the zero address.src/SimpleOraclePriceOracle.sol:IPriceOracleadapter over one feed. ImmutablesimpleOracle,feedId,collateralDecimals,maxAge,priceScale. The constructor reverts unless the store reports 8 decimals.price()revertsStalePricepastmaxAgeandInvalidAnsweron a non-positive answer, otherwise returnsanswer * 1e10.costis the sameMath.mulDivexpression as the Pyth and HyperCore adapters.script/DeploySimpleOracle.s.sol: standalone script. ReadscollateralAddressesandgeneralManagerAddressfromaddresses/addresses-<chainId>.json, requires the deployer to holdDEFAULT_ADMIN_ROLEon the GeneralManager, deploysSimpleOracle(SIMPLE_ORACLE_ADMIN, SIMPLE_ORACLE_SIGNER), then per collateralideploysSimpleOraclePriceOracle(simpleOracle, PYTH_PRICE_ID_i, COLLATERAL_DECIMALS_i, 60)(after checkingCOLLATERAL_DECIMALS_iagainst the token'sdecimals()) and callsGeneralManager.setPriceOracle. WritessimpleOracleAddressand replacespriceOraclesin the address book in collateral order, leaving every other key untouched..env.example:SIMPLE_ORACLE_ADMIN,SIMPLE_ORACLE_SIGNER.Testing :
forge test:Ran 60 test suites in 10.48s (86.57s CPU time): 460 tests passed, 0 failed, 2 skipped (462 total tests)(the two skips are the existing HyperCore fork tests withoutHYPERLIQUID_RPC).test/SimpleOracle.t.sol(33 tests: EIP-712 accept against an independently computed digest, wrong signer, tampered id/price/timestamp, malformed signature, older/equal/future timestamp, zero/negative price, check ordering, batch path incl. atomicity and same-feed ordering,UnknownFeed,setSignergating/zero address/rotation, and rejection of signatures made for another verifying contract or chain id, including aftervm.chainIdchanges),test/SimpleOraclePriceOracle.t.sol(15 tests: 8 -> 18 scaling with an exact value, staleness at exactlymaxAgeandmaxAge + 1, non-8-decimal store rejected, non-positive answer,UnknownFeedpropagation,costexact values and fuzzed parity withPythPriceOracle),test/script/DeploySimpleOracle.t.sol(4 tests: full run against a mock GeneralManager and an address book underaddresses/tests/, verifying wiring, order, and the rewritten JSON; length, admin, and decimals guards).forge fmt --checkwith forge 1.8.1,lintspec src, andsolhint 'src/**/*.sol'(5.1.0) all clean.Reviewers:
@SocksNFlops