Skip to content

BW-154: SimpleOracle signed price store and IPriceOracle adapter - #93

Merged
SocksNFlops merged 4 commits into
mainfrom
socks/bw-154-copy-oracle-contracts
Sep 3, 2026
Merged

BW-154: SimpleOracle signed price store and IPriceOracle adapter#93
SocksNFlops merged 4 commits into
mainfrom
socks/bw-154-copy-oracle-contracts

Conversation

@0xGumpy

@0xGumpy 0xGumpy commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Changes

  • src/interfaces/ISimpleOracle.sol: interface for the signed price store (decimals, signer, setSigner, latestRoundData, updatePrice, updatePrices, PRICE_UPDATE_TYPEHASH, plus the PriceUpdated / SignerUpdated events and InvalidSignature / InvalidTimestamp / InvalidPrice / UnknownFeed errors).
  • src/SimpleOracle.sol: the store. OpenZeppelin AccessControl (DEFAULT_ADMIN_ROLE to the constructor admin) and EIP712("SimpleOracle", "1"). updatePrice is permissionless and checks, in order, price > 0, timestamp <= block.timestamp && timestamp > stored, then that the EIP-712 signature over PriceUpdate(bytes32 id,int256 price,uint256 timestamp) recovers to signer. updatePrices(bytes[]) decodes each element as abi.encode(id, price, timestamp, signature) and applies the same logic. latestRoundData reverts UnknownFeed for a feed never set; the store does not enforce staleness. decimals() is 8 and prices are stored as signed. setSigner is admin-only and rejects the zero address.
  • src/SimpleOraclePriceOracle.sol: IPriceOracle adapter over one feed. Immutable simpleOracle, feedId, collateralDecimals, maxAge, priceScale. The constructor reverts unless the store reports 8 decimals. price() reverts StalePrice past maxAge and InvalidAnswer on a non-positive answer, otherwise returns answer * 1e10. cost is the same Math.mulDiv expression as the Pyth and HyperCore adapters.
  • script/DeploySimpleOracle.s.sol: standalone script. Reads collateralAddresses and generalManagerAddress from addresses/addresses-<chainId>.json, requires the deployer to hold DEFAULT_ADMIN_ROLE on the GeneralManager, deploys SimpleOracle(SIMPLE_ORACLE_ADMIN, SIMPLE_ORACLE_SIGNER), then per collateral i deploys SimpleOraclePriceOracle(simpleOracle, PYTH_PRICE_ID_i, COLLATERAL_DECIMALS_i, 60) (after checking COLLATERAL_DECIMALS_i against the token's decimals()) and calls GeneralManager.setPriceOracle. Writes simpleOracleAddress and replaces priceOracles in 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 without HYPERLIQUID_RPC).
  • New suites: 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, setSigner gating/zero address/rotation, and rejection of signatures made for another verifying contract or chain id, including after vm.chainId changes), test/SimpleOraclePriceOracle.t.sol (15 tests: 8 -> 18 scaling with an exact value, staleness at exactly maxAge and maxAge + 1, non-8-decimal store rejected, non-positive answer, UnknownFeed propagation, cost exact values and fuzzed parity with PythPriceOracle), test/script/DeploySimpleOracle.t.sol (4 tests: full run against a mock GeneralManager and an address book under addresses/tests/, verifying wiring, order, and the rewritten JSON; length, admin, and decimals guards).
  • forge fmt --check with forge 1.8.1, lintspec src, and solhint 'src/**/*.sol' (5.1.0) all clean.

Reviewers:

@SocksNFlops

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.
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
File % Lines % Statements % Branches % Funcs
script/BaseScript.s.sol 81.82% (18/22) 91.30% (21/23) 80.00% (4/5) 50.00% (2/4)
script/CollateralSetup.s.sol 57.45% (27/47) 78.18% (43/55) 50.00% (3/6) 0.00% (0/9)
script/DeployAll.s.sol 97.75% (87/89) 98.92% (92/93) 57.14% (4/7) 85.71% (6/7)
script/DeployConsol.s.sol 59.38% (19/32) 77.14% (27/35) N/A (0/0) 0.00% (0/6)
script/DeployForfeitedAssetsPool.s.sol 50.00% (10/20) 76.19% (16/21) N/A (0/0) 0.00% (0/5)
script/DeployGeneralManager.s.sol 69.77% (30/43) 90.57% (48/53) N/A (0/0) 0.00% (0/8)
script/DeployInterestOracle.s.sol 25.00% (3/12) 44.44% (4/9) N/A (0/0) 0.00% (0/4)
script/DeployLoanManager.s.sol 40.00% (6/15) 61.54% (8/13) N/A (0/0) 0.00% (0/4)
script/DeployMockStockTokens.s.sol 0.00% (0/16) 0.00% (0/21) 0.00% (0/2) 0.00% (0/2)
script/DeployNFTMetadataGenerator.s.sol 23.08% (3/13) 33.33% (3/9) 50.00% (1/2) 0.00% (0/4)
script/DeployOrderPool.s.sol 59.09% (13/22) 78.26% (18/23) 100.00% (1/1) 0.00% (0/4)
script/DeployOriginationScheduler.s.sol 66.67% (22/33) 84.85% (28/33) N/A (0/0) 0.00% (0/6)
script/DeployPriceOracles.s.sol 51.85% (14/27) 75.00% (21/28) 50.00% (1/2) 0.00% (0/6)
script/DeployQueues.s.sol 65.38% (34/52) 86.44% (51/59) N/A (0/0) 0.00% (0/10)
script/DeploySimpleOracle.s.sol 92.00% (46/50) 98.25% (56/57) 71.43% (10/14) 57.14% (4/7)
script/DeploySubConsols.s.sol 62.16% (23/37) 85.42% (41/48) 50.00% (1/2) 0.00% (0/7)
script/DeployUSDX.s.sol 57.14% (12/21) 78.26% (18/23) N/A (0/0) 0.00% (0/4)
script/LocalhostSetupPart1.s.sol 100.00% (24/24) 100.00% (27/27) N/A (0/0) 100.00% (2/2)
script/LocalhostSetupPart2.s.sol 100.00% (42/42) 100.00% (49/49) N/A (0/0) 100.00% (3/3)
script/LocalhostSetupPart3.s.sol 100.00% (18/18) 100.00% (21/21) N/A (0/0) 100.00% (2/2)
script/RemoveUsdh.s.sol 0.00% (0/13) 0.00% (0/15) 0.00% (0/10) 0.00% (0/1)
src/Consol.sol 87.50% (28/32) 87.88% (29/33) 0.00% (0/4) 100.00% (6/6)
src/ConversionQueue.sol 94.52% (69/73) 95.92% (94/98) 69.23% (9/13) 100.00% (7/7)
src/ForfeitedAssetsPool.sol 90.70% (39/43) 95.35% (41/43) 85.71% (6/7) 90.00% (9/10)
src/ForfeitedAssetsQueue.sol 94.74% (18/19) 95.45% (21/22) 66.67% (2/3) 100.00% (2/2)
src/GeneralManager.sol 94.04% (268/285) 94.20% (260/276) 68.57% (24/35) 95.45% (63/66)
src/HyperCorePriceOracle.sol 100.00% (36/36) 100.00% (44/44) 100.00% (8/8) 100.00% (4/4)
src/LenderQueue.sol 89.36% (42/47) 93.88% (46/49) 75.00% (6/8) 90.00% (9/10)
src/LoanManager.sol 98.36% (120/122) 98.18% (108/110) 86.67% (13/15) 100.00% (27/27)
src/MortgageNFT.sol 100.00% (22/22) 100.00% (17/17) 100.00% (2/2) 100.00% (7/7)
src/MortgageQueue.sol 98.44% (63/64) 97.01% (65/67) 100.00% (21/21) 100.00% (8/8)
src/MultiTokenVault.sol 92.42% (61/66) 92.86% (65/70) 54.55% (6/11) 100.00% (15/15)
src/OrderPool.sol 96.77% (60/62) 96.77% (60/62) 81.82% (9/11) 100.00% (11/11)
src/OriginationPool.sol 100.00% (59/59) 98.39% (61/62) 100.00% (12/12) 100.00% (10/10)
src/OriginationPoolScheduler.sol 94.57% (122/129) 94.96% (132/139) 81.82% (9/11) 90.32% (28/31)
src/PythInterestRateOracle.sol 77.27% (17/22) 78.26% (18/23) 50.00% (4/8) 100.00% (2/2)
src/PythPriceOracle.sol 80.95% (17/21) 80.00% (16/20) 25.00% (1/4) 100.00% (3/3)
src/QueueProcesssor.sol 100.00% (4/4) 100.00% (2/2) N/A (0/0) 100.00% (2/2)
src/RebasingERC20.sol 95.65% (22/23) 100.00% (22/22) 100.00% (5/5) 83.33% (5/6)
src/SimpleOracle.sol 100.00% (39/39) 100.00% (37/37) 100.00% (6/6) 100.00% (8/8)
src/SimpleOraclePriceOracle.sol 100.00% (21/21) 100.00% (22/22) 100.00% (3/3) 100.00% (3/3)
src/StaticInterestRateOracle.sol 100.00% (7/7) 100.00% (8/8) 100.00% (1/1) 100.00% (2/2)
src/SubConsol.sol 100.00% (41/41) 100.00% (37/37) 100.00% (6/6) 100.00% (9/9)
src/USDX.sol 97.87% (46/47) 100.00% (64/64) 100.00% (5/5) 88.89% (8/9)
src/UsdxQueue.sol 94.74% (18/19) 95.45% (21/22) 66.67% (2/3) 100.00% (2/2)
src/libraries/MortgageMath.sol 96.89% (156/161) 97.74% (173/177) 91.18% (31/34) 96.30% (26/27)
src/libraries/SharesMath.sol 93.33% (14/15) 90.91% (20/22) 83.33% (5/6) 100.00% (3/3)
src/types/OPoolConfigId.sol 100.00% (2/2) 100.00% (2/2) N/A (0/0) 100.00% (1/1)
test/BaseTest.t.sol 72.13% (132/183) 69.40% (127/183) 60.00% (3/5) 93.33% (14/15)
test/SimpleOraclePriceOracle.t.sol 100.00% (7/7) 100.00% (4/4) N/A (0/0) 100.00% (3/3)
test/integration/10_complexHistory.t.sol 99.60% (500/502) 100.00% (529/529) 100.00% (2/2) 33.33% (1/3)
test/integration/11_conversionRefinance.t.sol 98.37% (181/184) 99.47% (186/187) 100.00% (1/1) 33.33% (1/3)
test/integration/12_compoundingHalfConvert.t.sol 98.60% (141/143) 100.00% (149/149) N/A (0/0) 33.33% (1/3)
test/integration/13_convertedWithPenalties.t.sol 97.78% (132/135) 99.28% (138/139) 100.00% (1/1) 33.33% (1/3)
test/integration/14_compoundingNoPlanConversion.t.sol 98.71% (153/155) 100.00% (159/159) N/A (0/0) 33.33% (1/3)
test/integration/15_halfConverted.t.sol 98.47% (129/131) 100.00% (135/135) N/A (0/0) 33.33% (1/3)
test/integration/16_redeemAndDequeue.t.sol 98.13% (105/107) 100.00% (110/110) N/A (0/0) 33.33% (1/3)
test/integration/17_forecloseAndDequeue.t.sol 98.15% (106/108) 100.00% (111/111) N/A (0/0) 33.33% (1/3)
test/integration/18_expandLowerConvert.t.sol 99.06% (210/212) 100.00% (220/220) N/A (0/0) 50.00% (2/4)
test/integration/19_enqueueAfterCreation.t.sol 97.89% (93/95) 100.00% (96/96) N/A (0/0) 33.33% (1/3)
test/integration/1_foreclosure.t.sol 98.36% (120/122) 100.00% (123/123) N/A (0/0) 33.33% (1/3)
test/integration/20_completeConversion.t.sol 98.48% (130/132) 100.00% (136/136) N/A (0/0) 33.33% (1/3)
test/integration/21_noRedeemDequeue.t.sol 97.92% (94/96) 100.00% (99/99) N/A (0/0) 33.33% (1/3)
test/integration/22_ogPoolDonation.t.sol 95.00% (38/40) 100.00% (37/37) N/A (0/0) 33.33% (1/3)
test/integration/23_requestWithGasFees.t.sol 96.00% (48/50) 100.00% (49/49) N/A (0/0) 33.33% (1/3)
test/integration/24_conversionSmallAmount.t.sol 98.26% (113/115) 100.00% (124/124) N/A (0/0) 33.33% (1/3)
test/integration/25_usdxQueueCancel.t.sol 95.12% (39/41) 100.00% (42/42) N/A (0/0) 33.33% (1/3)
test/integration/26_forfeitedAssetsQueueCancel.t.sol 97.65% (83/85) 100.00% (89/89) N/A (0/0) 33.33% (1/3)
test/integration/27_donationBeforeOrigination.sol 97.87% (92/94) 100.00% (95/95) N/A (0/0) 33.33% (1/3)
test/integration/28_partialConvertAndForeclosure.sol 98.35% (119/121) 100.00% (121/121) N/A (0/0) 33.33% (1/3)
test/integration/2_payAndRefinance.t.sol 98.43% (125/127) 100.00% (135/135) N/A (0/0) 33.33% (1/3)
test/integration/3_payAndPenaltyImposed.t.sol 98.47% (129/131) 100.00% (136/136) N/A (0/0) 33.33% (1/3)
test/integration/4_orderExpires.t.sol 97.44% (76/78) 100.00% (77/77) N/A (0/0) 33.33% (1/3)
test/integration/5_usdxMaximumCap.t.sol 97.92% (94/96) 100.00% (98/98) N/A (0/0) 33.33% (1/3)
test/integration/6_slowForeclosure.t.sol 97.62% (82/84) 100.00% (85/85) N/A (0/0) 33.33% (1/3)
test/integration/7_usdxDonation.t.sol 90.00% (18/20) 100.00% (17/17) N/A (0/0) 33.33% (1/3)
test/integration/8_consolDonation.t.sol 93.75% (30/32) 100.00% (31/31) N/A (0/0) 33.33% (1/3)
test/integration/9_usdxWithdraw.t.sol 96.55% (84/87) 98.90% (90/91) 100.00% (1/1) 33.33% (1/3)
test/mocks/MockERC20.sol 75.00% (6/8) 75.00% (3/4) N/A (0/0) 75.00% (3/4)
test/mocks/MockGatedERC20.sol 0.00% (0/14) 0.00% (0/13) 0.00% (0/2) 0.00% (0/4)
test/mocks/MockHyperCorePrecompile.sol 100.00% (10/10) 100.00% (9/9) 100.00% (1/1) 100.00% (3/3)
test/mocks/MockLenderQueue.sol 88.24% (15/17) 90.00% (18/20) 33.33% (1/3) 100.00% (2/2)
test/mocks/MockMortgageQueue.sol 100.00% (7/7) 100.00% (4/4) N/A (0/0) 100.00% (4/4)
test/mocks/MockNFTMetadataGenerator.sol 100.00% (4/4) 100.00% (2/2) N/A (0/0) 100.00% (2/2)
test/mocks/MockOriginationPoolDeployCallback.sol 100.00% (7/7) 100.00% (5/5) N/A (0/0) 100.00% (3/3)
test/mocks/MockOriginationPoolSchedulerUpgraded.sol 100.00% (2/2) 100.00% (1/1) N/A (0/0) 100.00% (1/1)
test/mocks/MockRouter.sol 50.00% (34/68) 54.29% (38/70) 23.08% (3/13) 54.55% (6/11)
test/mocks/MockWrapper.sol 48.39% (15/31) 53.85% (14/26) 14.29% (1/7) 37.50% (3/8)
test/mocks/MockYieldStrategy.sol 80.00% (8/10) 71.43% (5/7) 0.00% (0/2) 100.00% (3/3)
test/script/DeployAllRoleInvariants.t.sol 94.12% (16/17) 90.48% (19/21) 100.00% (1/1) 100.00% (3/3)
test/script/DeploySimpleOracle.t.sol 100.00% (6/6) 100.00% (3/3) N/A (0/0) 100.00% (3/3)
Total 92.33% (5395/5843) 94.87% (5746/6057) 71.08% (236/332) 69.27% (381/550)

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.
@0xGumpy 0xGumpy changed the title BW-154: CopyOracle signed price store and IPriceOracle adapter BW-154: SimpleOracle signed price store and IPriceOracle adapter Sep 3, 2026
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
SocksNFlops merged commit dfbaff0 into main Sep 3, 2026
7 checks passed
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.

2 participants