Calculated from recorded token losses using historical USD prices at the incident time.
0x762881b07feb63c436dee38edd4ff1f7a74c33091e534af56c9f7d49b5ecac150x77f973fcaf871459aa58cd81881ce453759281bcEthereum0x1cf226e9413addaf22412a2e182f9c0de44af002EthereumIn 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.
Key system components involved:
0x77f973fcaf871459aa58cd81881ce453759281bc. iETH exposes a flash-loan-like primitive flashBorrowToken(...) that transfers the loan token and then executes an arbitrary callback via ArbitraryCaller.sendCall.LoanTokenLogicV4, which calls into bZx core (bZxProxy at 0x1cf226e9413addaf22412a2e182f9c0de44af002) for borrow sizing via getBorrowAmount(...), passing an oracle address (bZxOracle).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.0x172e09691dfbbc035e37c73b62095caa16ee2388 to exchange ETH for sUSD, enabling the attacker to obtain sufficient sUSD collateral on-chain within the same transaction.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:
borrowAmount can 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.
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
);
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..."
}
0x809a9e55 is getExpectedRate(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.
The exploit combines these properties:
borrowAmount using an external oracle path (bZx oracle integration).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.
0xb8c6ad5fe7cb6cc72f2c4196dca11fbb272a8cbf0x360f85f0b74326cddff33a812b05353bc537747b0x77f973fcaf871459aa58cd81881ce453759281bc0x000f400e6818158d541c3ebe45fe3aa0d47372ff0x818e6fecd516ecc3849daf6845e3ec868087b7550x172e09691dfbbc035e37c73b62095caa16ee23880xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc20x57ab1ec28d129707052df4df418d58a2d46d5f51All steps below occur within the single seed transaction 0x762881...ac15.
Initiate flash borrow from iETH
iETH.flashBorrowToken(borrowAmount=7500e18, borrower=0x360f85..., target=0x360f85..., ...).7,500 WETH to 0x360f85... and executes an arbitrary callback via ArbitraryCaller.sendCall.Convert WETH to ETH for trading
WETH.withdraw(7500e18) to obtain 7,500 ETH for downstream swaps.Manipulate sUSD pricing inputs via Kyber/Uniswap
swapEtherToToken(sUSD, ...) 19 times: one call with 540 ETH and eighteen calls with 20 ETH.Acquire additional sUSD via Synthetix Depot
exchangeEtherForSynths() with 6,000 ETH, receiving a large sUSD balance.Borrow against manipulated collateral valuation
iETH.borrowTokenFromDeposit(borrowAmount=0, leverageAmount=2e18, initialLoanDuration=604800, collateralTokenSent=<sUSD amount>, borrower=0x360f85..., receiver=0x360f85..., collateralTokenAddress=sUSD, ...).borrowAmount=0, iETH computes the borrow size using _getBorrowAmountForDeposit, which calls bZxProxy.getBorrowAmount(..., bZxOracle, ...).getExpectedRate and Uniswap V1 spot-price functions in the same transaction (Section 4.2).Payout + unwrap to ETH
6,796.012817135270528952 WETH to WethHelper at 0x3b5bdccdfa2a0a1911984f203c19628eeb6036e0 and calls claimEther(receiver=0x360f85..., amount=6796012817135270528952).WethHelper unwraps WETH and sends the equivalent ETH to the attacker-controlled contract.Repay the flash borrow
7,500 WETH by calling WETH.deposit{value: 7500 ETH}() and WETH.transfer(iETH, 7500e18).Profit realization
Measured value movements (from prestateTracer-based native balance deltas and observed WETH unwrap):
6,796.012817135270528952 WETH unwrapped to ETH.0x360f85... native balance delta: +2,378.153715421627764132 ETH.0xb8c6ad... gas cost: -0.41988685 ETH.+2,377.733828571627764132 ETH.https://etherscan.io/tx/0x762881b07feb63c436dee38edd4ff1f7a74c33091e534af56c9f7d49b5ecac150x77f973fcaf871459aa58cd81881ce453759281bc0x1cf226e9413addaf22412a2e182f9c0de44af0020x818e6fecd516ecc3849daf6845e3ec868087b7550x172e09691dfbbc035e37c73b62095caa16ee23880xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc20x57ab1ec28d129707052df4df418d58a2d46d5f51Supporting artifacts used for deterministic validation:
/workspace/session/artifacts/auditor/iter_0/tx_calltrace.json/workspace/session/artifacts/auditor/iter_0/tx_receipt.json/workspace/session/artifacts/collector/seed/1/0x762881b07feb63c436dee38edd4ff1f7a74c33091e534af56c9f7d49b5ecac15/balance_diff.json/workspace/session/.tmp/src_ieth_impl/LoanTokenLogicV4/Contract.sol