On Ethereum mainnet, transaction 0x770bc9a1f7c32cb63a5002b9ceb5c7994cd3af0fc6b2309cb32d3c46f629daa0 succeeded at block 25030409 on 2026-05-05T17:50:35Z. The attacker repeatedly invoked EkuboCore.lock(), withdraw(), and pay(), making the transaction appear tied to Ekubo’s flash-accounting path. The on-chain balance deltas show a narrower loss source: Ekubo Core finished the transaction with zero net WBTC change, while EOA 0x765decf4fa157756e850c1079f60801b9219edd1 lost 17 WBTC after previously granting malicious contract 0x8ccb1ffd5c2aa6bd926473425dea4c8c15de60fd an unlimited WBTC allowance. The attacker used Ekubo’s flash-accounting flow as an execution rail to pull WBTC from that approved victim and deliver the same amount to attacker EOA 0xa911ff351b143634dbc5af3e204ea074583a83e3, for an approximate loss of $1.4M.
Root Cause
Vulnerable Contract
- No verified vulnerability is evidenced in
EkuboCore0xe0e0e08a6a4b9dc7bd67bcb7aade5cf48157d444in this transaction. - The primary loss source is malicious callback contract
0x8ccb1ffd5c2aa6bd926473425dea4c8c15de60fd(AttackLockCallbackin the recovered artifacts). - Proxy status: none for both
0x8ccb...and0xe0e0.... - Source type:
0x8ccb1ffd5c2aa6bd926473425dea4c8c15de60fd: recovered[approximation]0xe0e0e08a6a4b9dc7bd67bcb7aade5cf48157d444: verified
- Attack vector classification:
access_control- More precisely, this is a standing approval drain. The decisive authorization was the victim’s pre-existing WBTC allowance to the malicious contract, not a missing check in Ekubo Core.
Vulnerable Function
- Primary drain primitive:
payCallback(uint256,address)selector0x599d0714on0x8ccb1ffd5c2aa6bd926473425dea4c8c15de60fd[recovered - approximation] - Supporting protocol functions:
withdraw(address,address,uint128)selector0x03a65ab6pay(address)selector0x0c11deddlock()selector0xf83d08ba
- These supporting functions live in verified file
src/base/FlashAccountant.solunder the Ekubo Core source tree.
Vulnerable Code
Recovered malicious callback logic:
// [recovered — approximation]
function payCallback(uint256 id, address token) external {
require(msg.sender == address(CORE), "core only");
id;
if (msg.data.length >= 132) {
uint256 amount;
address source;
assembly ("memory-safe") {
amount := calldataload(0x64)
source := shr(96, calldataload(0x84))
}
if (amount != 0) {
IERC20Like(token).transferFrom(source, address(CORE), amount); // <-- VULNERABILITY
return;
}
}
revert("unrecovered callback branch");
}
Verified Ekubo flash-accounting path used by the malicious contract:
function pay(address token) external returns (uint128 payment) {
(uint256 id,) = _getLocker();
assembly ("memory-safe") {
let free := mload(0x40)
mstore(20, address())
mstore(0, 0x70a08231000000000000000000000000)
let tokenBalanceBefore :=
mul(
mload(free),
and(
gt(returndatasize(), 0x1f),
staticcall(gas(), token, 0x10, 0x24, free, 0x20)
)
)
mstore(free, shl(224, 0x599d0714))
mstore(add(free, 4), id)
mstore(add(free, 36), token)
calldatacopy(add(free, 68), 36, sub(calldatasize(), 36)) // <-- attacker-controlled tail calldata forwarded
if iszero(call(gas(), caller(), 0, free, add(32, calldatasize()), 0, 0)) {
returndatacopy(free, 0, returndatasize())
revert(free, returndatasize())
}
let tokenBalanceAfter :=
mul(
mload(0x20),
and(
gt(returndatasize(), 0x1f),
staticcall(gas(), token, 0x10, 0x24, 0x20, 0x20)
)
)
payment := sub(tokenBalanceAfter, tokenBalanceBefore) // <-- only Core balance delta is enforced
}
unchecked {
_accountDebt(id, token, -int256(uint256(payment)));
}
}
function withdraw(address token, address recipient, uint128 amount) external {
(uint256 id,) = _requireLocker();
_accountDebt(id, token, int256(uint256(amount)));
if (token == NATIVE_TOKEN_ADDRESS) {
SafeTransferLib.safeTransferETH(recipient, amount);
} else {
SafeTransferLib.safeTransfer(token, recipient, amount); // <-- withdrawn WBTC is sent to attacker-controlled recipient
}
}
Why It’s Vulnerable
The decisive weakness in this transaction is not a broken Ekubo invariant; it is the victim’s standing approval to the malicious callback contract.
Expected behavior:
- A caller that wants to use Ekubo flash accounting should repay its own temporary debt with its own funds, or with funds from an address that intentionally approved that caller.
- If no third party has approved the callback contract, the callback cannot fund
EkuboCore.pay()from that third party. - If Ekubo itself were losing funds,
funds_flow.jsonwould show a negative net WBTC change for0xe0e0....
Actual behavior:
- At block
25030408,allowance(0x765decf4fa157756e850c1079f60801b9219edd1, 0x8ccb1ffd5c2aa6bd926473425dea4c8c15de60fd)was2^256 - 1. - At the same block,
allowance(0x765decf4fa157756e850c1079f60801b9219edd1, 0xe0e0e08a6a4b9dc7bd67bcb7aade5cf48157d444)was0. - The decoded trace shows
payCallback(uint256,address)callingWBTC.transferFrom(0x765d..., 0xe0e0..., 0.2 WBTC)85times, whileEkuboCore.withdraw(WBTC, attackerEOA, 0.2 WBTC)sends the matching0.2 WBTCto the attacker EOA each time. funds_flow.jsonshows the resulting net deltas exactly: attacker EOA+17WBTC, victim EOA-17WBTC, Ekubo Core0WBTC net.
Normal flow vs attack flow:
Normal flow:
- Locker enters
lock(). - Locker temporarily withdraws tokens from Core.
- Locker’s callback repays Core from the locker’s own wallet or a deliberately approved funding address.
- Debt returns to zero and the lock exits.
- Locker enters
Attack flow:
- Attacker-controlled
AttackDispatchercallsAttackLockCallback85times. AttackLockCallbackwithdraws0.2WBTC from Ekubo Core to attacker EOA on each iteration.- In
payCallback, the same contract uses the victim EOA’s pre-existing unlimited WBTC approval to refill Core with0.2WBTC from0x765d.... - Ekubo’s debt accounting is satisfied because Core’s own WBTC balance is restored, but the victim has funded the repayment and the attacker keeps the withdrawn WBTC.
- Attacker-controlled
This matters because it makes the transaction look like an Ekubo exploit while the actual economic loss is an authorization drain from the victim EOA. The verified Ekubo code is functioning consistently with its flash-accounting model in this trace; the malicious callback contract abuses an off-protocol approval to settle its Ekubo debt with third-party funds.
Validation Against Alternative Root Causes
Primary on-chain artifacts and historical chain state support the approval-drain conclusion independently of the recovered attacker-contract pseudocode:
- Receipt-level WBTC transfers show exactly two repeated legs:
85transfers from Ekubo Core to attacker EOA, and85transfers from victim EOA back to Ekubo Core. Each transfer is20_000_000raw WBTC units (0.2WBTC), for a total of17WBTC in each direction. - Historical WBTC balances around the exploit block confirm the same accounting: victim
0x765d...changed from1,701,484,735to1,484,735raw WBTC (-17WBTC), attacker0xa911...changed from0to1,700,000,000raw WBTC (+17WBTC), and Ekubo Core stayed unchanged at32,349,396raw WBTC. - Historical allowance checks identify the authorization source. At block
25030408, the victim had unlimited WBTC allowance to0x8ccb..., zero allowance to Ekubo Core, and after the exploit the victim-to-0x8ccb...allowance decreased by exactly1,700,000,000raw units (17WBTC). - The decoded trace independently matches the mechanism: every
EkuboCore.pay(WBTC)call invokespayCallback(uint256,address)on0x8ccb..., and that callback immediately callsWBTC.transferFrom(victim, EkuboCore, 20_000_000). - The verified Ekubo source explains why the transaction succeeds without making Ekubo net-negative:
withdraw()records debt and sends tokens to the chosen recipient, whilepay()only requires Core’s token balance to increase during the payer callback.
These checks reject two alternative explanations. First, Ekubo Core was not the economic loss source in this transaction because its WBTC balance and net transfer delta are both unchanged. Second, the victim did not approve Ekubo Core; the consumed approval belonged to the malicious callback contract 0x8ccb....
Attack Execution
High-Level Flow
- The attacker EOA calls a small dispatcher contract with a payload pointing at a malicious callback contract.
- The dispatcher invokes that callback contract
85times in a loop. - On each iteration, the callback contract opens an Ekubo flash-accounting lock, tries an irrelevant forwarding branch that reverts, then withdraws
0.2WBTC from Ekubo Core to the attacker EOA. - The callback contract immediately asks Ekubo Core to settle the temporary WBTC debt.
- During the settlement callback, the malicious contract uses a victim EOA’s standing WBTC approval to pull
0.2WBTC from the victim back into Ekubo Core. - After
85iterations, the attacker EOA has accumulated17WBTC, the victim EOA has lost17WBTC, and Ekubo Core’s WBTC balance is unchanged.
Detailed Call Trace
The trace-derived call flow is:
0xa911ff351b143634dbc5af3e204ea074583a83e3
-> 0x61b0dad9628d3e644eb560a5c9b0f960430e3a75 func_0x718a549d(...) [CALL]
-> repeated 85 times:
-> 0x8ccb1ffd5c2aa6bd926473425dea4c8c15de60fd 0x00090905 [CALL]
-> 0xe0e0e08a6a4b9dc7bd67bcb7aade5cf48157d444 lock() [CALL]
-> 0x8ccb1ffd5c2aa6bd926473425dea4c8c15de60fd locked(uint256) [CALL]
-> 0xe0e0e08a6a4b9dc7bd67bcb7aade5cf48157d444 forward(address) [CALL, reverts]
-> 0x2260fac5e5542a773aa44fbcfedf7c193bc2c599 forwarded(uint256,address) [CALL, reverts]
-> 0xe0e0e08a6a4b9dc7bd67bcb7aade5cf48157d444 withdraw(address,address,uint128) [CALL]
-> 0x2260fac5e5542a773aa44fbcfedf7c193bc2c599 transfer(address,uint256) [CALL]
-> 0xe0e0e08a6a4b9dc7bd67bcb7aade5cf48157d444 pay(address) [CALL]
-> 0x2260fac5e5542a773aa44fbcfedf7c193bc2c599 balanceOf(address) [STATICCALL]
-> 0x8ccb1ffd5c2aa6bd926473425dea4c8c15de60fd payCallback(uint256,address) [CALL]
-> 0x2260fac5e5542a773aa44fbcfedf7c193bc2c599 transferFrom(address,address,uint256) [CALL]
-> 0x2260fac5e5542a773aa44fbcfedf7c193bc2c599 balanceOf(address) [STATICCALL]
Important trace facts:
forward(address)reverts85times, but the parentlocked(uint256)call does not revert. The malicious contract ignores that branch and continues withwithdraw()andpay().- The first decoded
withdraw()call iswithdraw(WBTC, attackerEOA, 20_000_000), i.e.0.2WBTC to0xa911.... - The first decoded
pay()call includes attacker-controlled trailing calldata after the token argument. - The first decoded
transferFrom()call istransferFrom(0x765decf4fa157756e850c1079f60801b9219edd1, 0xe0e0e08a6a4b9dc7bd67bcb7aade5cf48157d444, 20_000_000).
Financial Impact
- Attacker EOA
0xa911ff351b143634dbc5af3e204ea074583a83e3:+17WBTC - Victim/source EOA
0x765decf4fa157756e850c1079f60801b9219edd1:-17WBTC - Ekubo Core
0xe0e0e08a6a4b9dc7bd67bcb7aade5cf48157d444: net0WBTC - Approximate USD impact:
~$1.4M - Gas used:
1,735,786 - Effective gas price:
0.249694514 gwei - Gas cost:
0.000433416241678004ETH
Protocol solvency impact:
- This transaction does not show Ekubo Core losing custody of WBTC.
- The economic loss is borne by the approved victim EOA, not by Ekubo liquidity or treasury balances.
- The incident is better classified as a malicious approval drain routed through Ekubo than as an Ekubo insolvency event.
Evidence
funds_flow.jsonsummary:- attacker EOA gained
17WBTC - victim/source EOA lost
17WBTC - Ekubo Core net WBTC change is
0
- attacker EOA gained
- Pre-transaction allowance checks at block
25030408:allowance(0x765d..., 0x8ccb...) = 115792089237316195423570985008687907853269984665640564039457584007913129639935allowance(0x765d..., 0xe0e0...) = 0
- Post-transaction allowance checks at block
25030409:allowance(0x765d..., 0x8ccb...) = 115792089237316195423570985008687907853269984665640564039457584007911429639935- The decrease is exactly
1_700_000_000raw WBTC units (17WBTC), matching the victim’s loss.
- Historical WBTC balance deltas from block
25030408to block25030409:- victim/source EOA:
1_701_484_735 -> 1_484_735raw units (-17WBTC) - attacker EOA:
0 -> 1_700_000_000raw units (+17WBTC) - Ekubo Core:
32_349_396 -> 32_349_396raw units (0WBTC)
- victim/source EOA:
- Receipt log aggregation:
85WBTC transfers from0xe0e0...to0xa911..., total1_700_000_000raw units85WBTC transfers from0x765d...to0xe0e0..., total1_700_000_000raw units
- The decoded
transferFrom()calldata resolves to:from = 0x765DECF4Fa157756e850C1079F60801b9219Edd1to = 0xe0e0e08A6A4b9Dc7bD67BCB7aadE5cF48157d444amount = 20_000_000raw WBTC units (0.2WBTC)
- The transaction receipt has
status = 0x1and170logs. - No Tornado Cash interaction occurs inside the analyzed transaction. Later Tornado Cash deposits, if any, occur in subsequent transactions.
Related URLs
- Transaction: https://etherscan.io/tx/0x770bc9a1f7c32cb63a5002b9ceb5c7994cd3af0fc6b2309cb32d3c46f629daa0
- Attacker EOA: https://etherscan.io/address/0xa911ff351b143634dbc5af3e204ea074583a83e3
- Dispatcher contract: https://etherscan.io/address/0x61b0dad9628d3e644eb560a5c9b0f960430e3a75
- Callback contract: https://etherscan.io/address/0x8ccb1ffd5c2aa6bd926473425dea4c8c15de60fd
- Ekubo Core: https://etherscan.io/address/0xe0e0e08a6a4b9dc7bd67bcb7aade5cf48157d444
- Victim/source EOA: https://etherscan.io/address/0x765decf4fa157756e850c1079f60801b9219edd1
- WBTC token: https://etherscan.io/address/0x2260fac5e5542a773aa44fbcfedf7c193bc2c599