bZx/Fulcrum iETH oracle manipulation enables undercollateralized WETH borrowing
Exploit Transactions
0x762881b07feb63c436dee38edd4ff1f7a74c33091e534af56c9f7d49b5ecac15Victim Addresses
0x77f973fcaf871459aa58cd81881ce453759281bcEthereum0x1cf226e9413addaf22412a2e182f9c0de44af002EthereumLoss Breakdown
Similar Incidents
bZx/Fulcrum WBTC market manipulation drains ETH liquidity
46%Ploutos Market Oracle Feed Misconfiguration Enabled Undercollateralized WETH Borrow
37%Aave AMM LP Oracle Manipulation Through Delegated Recursive LP Looping
35%CauldronV4 solvency-check bypass enables uncollateralized MIM borrowing
33%SilicaPools decimal-manipulation bug drains WBTC flashloan collateral
31%WETH Drain via Unprotected 0xfa461e33 Callback on 0x03f9-62c0
31%Root Cause Analysis
bZx/Fulcrum iETH oracle manipulation enables undercollateralized WETH borrowing
1. Incident Overview TL;DR
In Ethereum mainnet block 9,504,627, transaction 0x762881b07feb63c436dee38edd4ff1f7a74c33091e534af56c9f7d49b5ecac15 exploited bZx/Fulcrum iETH by manipulating the protocol's sUSD/ETH pricing inputs and then borrowing against overvalued sUSD collateral.
The adversary used iETH's flashBorrowToken(uint256,address,address,string,bytes) to temporarily receive 7,500 WETH, executed a callback that (1) manipulated the Kyber/Uniswap V1 spot-derived rate for sUSD, (2) acquired a large sUSD balance via Synthetix Depot, and (3) invoked borrowTokenFromDeposit(...) with borrowAmount=0 so iETH computed the borrow size from the manipulated oracle path. iETH then paid out 6,796.012817135270528952 WETH (unwrapped to ETH via WethHelper) to the adversary-controlled contract, and the flash loan was repaid before the transaction completed.
The root cause is that borrow sizing for borrowTokenFromDeposit depended on an oracle path that reads manipulable same-transaction DEX spot prices (Kyber getExpectedRate and Uniswap V1 getEthToTokenInputPrice / getTokenToEthInputPrice). Because the attacker moved those prices immediately before the borrow sizing call in the same transaction, the protocol computed an undercollateralized borrow amount.
2. Key Background
Key system components involved:
- Victim lending market (iETH): Fulcrum iToken for ETH/WETH at
0x77f973fcaf871459aa58cd81881ce453759281bc. iETH exposes a flash-loan-like primitiveflashBorrowToken(...)that transfers the loan token and then executes an arbitrary callback viaArbitraryCaller.sendCall. - bZx core + oracle integration: iETH delegates to
LoanTokenLogicV4, which calls into bZx core (bZxProxyat0x1cf226e9413addaf22412a2e182f9c0de44af002) for borrow sizing viagetBorrowAmount(...), passing an oracle address (bZxOracle). - Oracle price source (manipulable): The observed borrow-sizing path queries Kyber's on-chain
getExpectedRate(and downstream logic that reads Uniswap V1 spot prices). These spot prices are functions of current pool reserves and are therefore manipulable intra-transaction by executing swaps. - Collateral acquisition (sUSD): The flow uses Synthetix Depot at
0x172e09691dfbbc035e37c73b62095caa16ee2388to exchange ETH for sUSD, enabling the attacker to obtain sufficient sUSD collateral on-chain within the same transaction.
3. Vulnerability Analysis & Root Cause Summary
This incident is an oracle-manipulation borrowing attack.
iETH's borrowTokenFromDeposit(...) can compute a borrow amount when borrowAmount=0. That computation calls bZx core getBorrowAmount(...) with the configured oracle address. In the seed transaction, the oracle evaluation path queries Kyber getExpectedRate(...), which (as observed in the trace) triggers Uniswap V1 spot-price reads (getEthToTokenInputPrice / getTokenToEthInputPrice). Because the attacker can trade against the relevant Uniswap V1 exchange just prior to this oracle query, the computed sUSD/ETH valuation is attacker-controlled within the same transaction.
The violated safety invariant is:
- When originating a borrow from deposit, the oracle rate used to value collateral must be manipulation-resistant; otherwise,
borrowAmountcan exceed what is economically collateralized.
The deterministic breakpoint in this exploit is the oracle query for borrow sizing that occurs after the attacker executes reserve-changing swaps in the same transaction, causing getBorrowAmount(...) to be computed from manipulated spot pricing.
4. Detailed Root Cause Analysis
4.1 Victim Code Path: Flash Borrow + Borrow From Deposit
The iETH implementation explicitly supports (a) transferring assets to a borrower, (b) executing an arbitrary callback, and then (c) checking that balances are restored.
Verified iETH LoanTokenLogicV4.flashBorrowToken (excerpt):
function flashBorrowToken(
uint256 borrowAmount,
address borrower,
address target,
string calldata signature,
bytes calldata data)
external
payable
nonReentrant
returns (bytes memory)
{
_checkPause();
_settleInterest();
uint256 beforeEtherBalance = address(this).balance.sub(msg.value);
uint256 beforeAssetsBalance = ERC20(loanTokenAddress).balanceOf(address(this))
.add(totalAssetBorrow);
burntTokenReserved = beforeAssetsBalance;
if (borrowAmount != 0) {
_transfer(loanTokenAddress, borrower, borrowAmount, "39");
}
// arbitrary call via ArbitraryCaller
(bool success, bytes memory returnData) = arbitraryCaller.call.value(msg.value)(
abi.encodeWithSelector(
0xde064e0d, // sendCall(address,bytes)
target,
callData
)
);
require(success, "call failed");
burntTokenReserved = 0;
require(
address(this).balance >= beforeEtherBalance &&
ERC20(loanTokenAddress).balanceOf(address(this)).add(totalAssetBorrow) >= beforeAssetsBalance,
"40"
);
return returnData;
}
The borrow sizing used when borrowAmount=0 in borrowTokenFromDeposit is computed via _getBorrowAmountForDeposit, which calls into bZx core and passes the oracle address (bZxOracle).
Verified iETH LoanTokenLogicV4._getBorrowAmountForDeposit (excerpt):
borrowAmount = IBZx(bZxContract).getBorrowAmount(
loanTokenAddress,
collateralTokenAddress != address(0) ? collateralTokenAddress : wethContract,
bZxOracle,
depositAmount,
marginAmount
);
4.2 Oracle Breakpoint: Same-Tx Kyber Expected Rate and Uniswap V1 Spot Reads
In the seed transaction trace, bZx core/oracle evaluation performs a Kyber getExpectedRate query and (within that call) reads Uniswap V1 spot prices.
Call trace excerpt (debug_traceTransaction callTracer), showing function selectors and targets:
{
"to": "0x818e6fecd516ecc3849daf6845e3ec868087b755",
"type": "STATICCALL",
"input": "0x809a9e55..."
}
0x809a9e55isgetExpectedRate(address,address,uint256).
Within the same oracle query path, the trace shows Uniswap V1 exchange spot-price function calls:
0xcd7724c3 // getEthToTokenInputPrice(uint256)
0x95b68fe7 // getTokenToEthInputPrice(uint256)
Because Uniswap V1 spot prices are direct functions of pool reserves at the moment of the call, and because the attacker makes reserve-changing trades immediately beforehand, the oracle value used for borrow sizing is attacker-controlled within the same transaction.
4.3 Why This Enables Undercollateralized Borrowing
The exploit combines these properties:
- iETH computes
borrowAmountusing an external oracle path (bZx oracle integration). - That oracle path reads same-transaction DEX spot prices (Kyber expected-rate route, including Uniswap V1 spot queries).
- The attacker can perform trades that move the relevant spot price immediately before the borrow-sizing call.
As a result, the computed borrow amount reflects an inflated valuation of the attacker's sUSD collateral. iETH then originates and pays out a large WETH amount that is not economically collateralized.
5. Adversary Flow Analysis
5.1 Participants
- Tx sender (EOA, gas payer):
0xb8c6ad5fe7cb6cc72f2c4196dca11fbb272a8cbf - Primary attacker-controlled contract (borrower/receiver):
0x360f85f0b74326cddff33a812b05353bc537747b - Victim iETH:
0x77f973fcaf871459aa58cd81881ce453759281bc - Arbitrary caller used by iETH:
0x000f400e6818158d541c3ebe45fe3aa0d47372ff - KyberNetworkProxy:
0x818e6fecd516ecc3849daf6845e3ec868087b755 - Synthetix Depot:
0x172e09691dfbbc035e37c73b62095caa16ee2388 - WETH:
0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 - sUSD:
0x57ab1ec28d129707052df4df418d58a2d46d5f51
5.2 End-to-End Transaction Flow (Single Transaction)
All steps below occur within the single seed transaction 0x762881...ac15.
-
Initiate flash borrow from iETH
- Call:
iETH.flashBorrowToken(borrowAmount=7500e18, borrower=0x360f85..., target=0x360f85..., ...). - Effect: iETH transfers
7,500 WETHto0x360f85...and executes an arbitrary callback viaArbitraryCaller.sendCall.
- Call:
-
Convert WETH to ETH for trading
- The borrower contract calls
WETH.withdraw(7500e18)to obtain7,500 ETHfor downstream swaps.
- The borrower contract calls
-
Manipulate sUSD pricing inputs via Kyber/Uniswap
- The borrower contract calls Kyber
swapEtherToToken(sUSD, ...)19 times: one call with540 ETHand eighteen calls with20 ETH. - These swaps change the Uniswap V1 exchange reserves used by Kyber's expected-rate logic and therefore change the spot-derived oracle rate.
- The borrower contract calls Kyber
-
Acquire additional sUSD via Synthetix Depot
- The borrower contract calls Synthetix Depot
exchangeEtherForSynths()with6,000 ETH, receiving a large sUSD balance.
- The borrower contract calls Synthetix Depot
-
Borrow against manipulated collateral valuation
- The borrower contract calls:
iETH.borrowTokenFromDeposit(borrowAmount=0, leverageAmount=2e18, initialLoanDuration=604800, collateralTokenSent=<sUSD amount>, borrower=0x360f85..., receiver=0x360f85..., collateralTokenAddress=sUSD, ...).
- Because
borrowAmount=0, iETH computes the borrow size using_getBorrowAmountForDeposit, which callsbZxProxy.getBorrowAmount(..., bZxOracle, ...). - During this sizing step, the oracle path queries Kyber
getExpectedRateand Uniswap V1 spot-price functions in the same transaction (Section 4.2).
- The borrower contract calls:
-
Payout + unwrap to ETH
- iETH transfers
6,796.012817135270528952 WETHtoWethHelperat0x3b5bdccdfa2a0a1911984f203c19628eeb6036e0and callsclaimEther(receiver=0x360f85..., amount=6796012817135270528952). WethHelperunwraps WETH and sends the equivalent ETH to the attacker-controlled contract.
- iETH transfers
-
Repay the flash borrow
- The borrower contract repays
7,500 WETHby callingWETH.deposit{value: 7500 ETH}()andWETH.transfer(iETH, 7500e18).
- The borrower contract repays
-
Profit realization
- The attacker-controlled contract ends the transaction with a large net ETH increase (see Section 6).
6. Impact & Losses
Measured value movements (from prestateTracer-based native balance deltas and observed WETH unwrap):
- iETH economic loss (payout attributable to the undercollateralized borrow):
6,796.012817135270528952 WETHunwrapped to ETH. - Attacker profit (native ETH):
- Attacker receiver contract
0x360f85...native balance delta:+2,378.153715421627764132 ETH. - Tx sender
0xb8c6ad...gas cost:-0.41988685 ETH. - Net cluster delta (receiver profit minus gas):
+2,377.733828571627764132 ETH.
- Attacker receiver contract
7. References
- Seed transaction:
https://etherscan.io/tx/0x762881b07feb63c436dee38edd4ff1f7a74c33091e534af56c9f7d49b5ecac15 - Victim iETH contract:
0x77f973fcaf871459aa58cd81881ce453759281bc - bZxProxy (core):
0x1cf226e9413addaf22412a2e182f9c0de44af002 - KyberNetworkProxy:
0x818e6fecd516ecc3849daf6845e3ec868087b755 - Synthetix Depot:
0x172e09691dfbbc035e37c73b62095caa16ee2388 - WETH:
0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 - sUSD:
0x57ab1ec28d129707052df4df418d58a2d46d5f51
Supporting artifacts used for deterministic validation:
- Call trace (debug_traceTransaction callTracer):
/workspace/session/artifacts/auditor/iter_0/tx_calltrace.json - Receipt logs:
/workspace/session/artifacts/auditor/iter_0/tx_receipt.json - Balance diffs (prestateTracer diffMode):
/workspace/session/artifacts/collector/seed/1/0x762881b07feb63c436dee38edd4ff1f7a74c33091e534af56c9f7d49b5ecac15/balance_diff.json - Verified iETH implementation (LoanTokenLogicV4):
/workspace/session/.tmp/src_ieth_impl/LoanTokenLogicV4/Contract.sol