0xa05f047ddfdad9126624c4496b5d4a59f961ee7c091e7b4e38cee86f1335736f0xB91AE2c8365FD45030abA84a4666C4dB074E53E7EthereumOn Ethereum mainnet block 22157900, transaction 0xa05f047ddfdad9126624c4496b5d4a59f961ee7c091e7b4e38cee86f1335736f exploited the Synthetics vault at 0xB91AE2c8365FD45030abA84a4666C4dB074E53E7. The adversary created a fake vault pair, executed a legitimate first-mint path, and used that path to overwrite the vault's callback-authentication transient storage slot with an attacker-chosen mint amount. The attacker then deployed a helper at 0x00000000001271551295307aCC16bA1e7E0d4281, which equals address(uint160(95759995883742311247042417521410689)), and sent forged uniswapV3SwapCallback calls that drained the vault's USDC, WBTC, and WETH.
The root cause is a code defect in the victim vault, not a market-side MEV opportunity. Vault.mint() stores the expected Uniswap pool address in transient slot 1, but Vault.uniswapV3SwapCallback() later reuses the same slot to store the minted amount. That slot alias converts a caller-authentication value into attacker-controlled data.
The victim vault supports minting synthetic assets by swapping through Uniswap V3. When a user mints with the debt token, Vault.mint() fetches the relevant pool and stages the swap callback by placing the pool address into transient storage slot 1. The callback then authenticates the caller with require(msg.sender == tload(1)).
The same callback also acts as a scratch-space bridge back to : after finishing the mint logic, it writes the minted amount into transient slot , and later reads that slot to recover the return value. The attack is possible because these two meanings share the same slot.
mint()1mint()The first mint for a fresh APE market is attacker-steerable. In APE.mint(), when totalSupply == 0, the minted amount is fees.collateralInOrWithdrawn + reserves.reserveApes, so an attacker who initializes a fake pair and controls the first mint conditions can choose the resulting amount.
The vulnerability class is an authentication bypass caused by transient-storage slot aliasing. The vault relies on a single transient slot for two incompatible roles: callback authorization and temporary return-value transport. That design is unsafe because the callback itself can replace the authenticated pool address with attacker-influenced state before all external entry points are closed.
The critical breakpoint is in Vault.uniswapV3SwapCallback(), where the function ends with tstore(1, amount). Earlier in the same flow, Vault.mint() had already written tstore(1, uniswapPool). After the legitimate first callback returns, slot 1 no longer contains the pool address; it contains the freshly minted APE amount. If the attacker can make that amount equal an address they control, the next direct external call to uniswapV3SwapCallback() passes the vault's only caller-authentication check.
This is exactly what happened in the incident. The collector trace and reproduced fork execution both show the first mint returning 95759995883742311247042417521410689, and that 160-bit value corresponds to helper address 0x00000000001271551295307aCC16bA1e7E0d4281. Once the slot was poisoned, forged callbacks transferred vault-held assets out before any real in-flight-swap provenance was established.
The vulnerable code path in the victim vault is:
// Vault.mint()
assembly {
tstore(1, uniswapPool)
}
...
(int256 amount0, int256 amount1) = IUniswapV3Pool(uniswapPool).swap(...);
...
assembly {
amount := tload(1)
}
// Vault.uniswapV3SwapCallback()
address uniswapPool;
assembly {
uniswapPool := tload(1)
}
require(msg.sender == uniswapPool);
...
uint256 amount = _mint(minter, ape, vaultParams, uint144(collateralToDeposit), vaultState, reserves);
...
assembly {
tstore(1, amount)
}
This code creates a direct state-alias bug. The callback authorization invariant should be: "slot 1 must remain the expected Uniswap pool address for the duration of all externally reachable callback checks." The implementation breaks that invariant by overwriting the slot with amount.
The attacker made the overwritten amount useful by controlling the first mint of a fake APE market. In APE.sol, the first mint path is:
amount = supplyAPE == 0
? fees.collateralInOrWithdrawn + reserves.reserveApes
: FullMath.mulDiv(supplyAPE, fees.collateralInOrWithdrawn, reserves.reserveApes);
Because supplyAPE == 0 on the fresh fake pair, the attacker can tune the initial reserves and swap inputs so the first minted amount equals a deployable helper address. The collected trace records that mint result as 95759995883742311247042417521410689, and the same transaction later uses helper address 0x00000000001271551295307aCC16bA1e7E0d4281, which is address(uint160(95759995883742311247042417521410689)).
After poisoning slot 1, the attacker invoked forged callbacks carrying victim asset addresses as debtToken. The collector trace shows the drain sequence:
Vault::uniswapV3SwapCallback(0, 17814862676, ...)
0xA0b86991...::transfer(0x00000000001271551295307aCC16bA1e7E0d4281, 17814862676)
Vault::uniswapV3SwapCallback(0, 140852920, ...)
0x2260FAC5...::transfer(0xeA55fFFAe1937E47eBA2D854ab7bd29a9CC29170, 140852920)
Vault::uniswapV3SwapCallback(0, 119871037891574186422, ...)
0xC02aaA39...::transfer(..., 119871037891574186422)
The USDC balance diff confirms the vault lost 17814862676 raw USDC units and the attacker EOA gained the same amount. The root cause is therefore fully determined: slot reuse destroyed callback-auth integrity, and forged callbacks converted that integrity failure into direct token transfers.
The exploit is a single attacker-crafted transaction with three stages.
First, the adversary EOA 0x27defcfa6498f957918f407ed8a58eba2884768c called orchestrator 0xea55fffae1937e47eba2d854ab7bd29a9cc29170. The orchestrator deployed fake collateral and debt tokens, created a Uniswap V3 pool for that fake pair, and initialized a new vault entry on the victim singleton.
Second, the orchestrator executed a legitimate victimVault.mint(true, ...) against the fake pair. That legitimate path reached the vulnerable callback and ended by writing the first mint amount into transient slot 1. The collector trace records:
Vault::uniswapV3SwapCallback(139650998347915452795864661928406629, -114911995060490773496450901025692827, ...)
emit Transfer(..., dst: 0xeA55fFFAe1937E47eBA2D854ab7bd29a9CC29170, wad: 95759995883742311247042417521410689)
emit Mint(vaultId: 21, isAPE: true, collateralIn: 95759995883742311247042417521410689, ...)
Third, the attacker used the helper deployed at address(uint160(firstMintAmount)) to make three direct uniswapV3SwapCallback calls. Each call set isETH=true and named a real vault-held asset as debtToken, causing the vault to execute the transfer path intended only for a legitimate swap callback. The helper then forwarded the drained balances to the attacker EOA.
This flow satisfies the ACT model. It requires no privileged keys, no private orderflow, and no victim-side permissions. An unprivileged actor can reproduce it from public state by constructing the same fake-pair and first-mint conditions.
The victim vault lost all three supported assets it held at the exploit point:
17814862676 raw units (17,814.862676 USDC, decimal=6)140852920 raw units (1.40852920 WBTC, decimal=8)119871037891574186422 raw units (119.871037891574186422 WETH, decimal=18)The attacker EOA also paid 25754539000000000 wei in gas, as shown in the native balance delta artifact. The measurable impact is direct vault asset depletion through unauthorized callback-triggered transfers.
0xa05f047ddfdad9126624c4496b5d4a59f961ee7c091e7b4e38cee86f1335736f0xB91AE2c8365FD45030abA84a4666C4dB074E53E70x27defcfa6498f957918f407ed8a58eba2884768c0xea55fffae1937e47eba2d854ab7bd29a9cc291700x00000000001271551295307aCC16bA1e7E0d4281Vault.mint() and Vault.uniswapV3SwapCallback() in the collected Vault.solAPE.sol