VRF Reconfiguration LINK Drain
Exploit Transactions
0x103b4550a1a2bdb73e3cb5ea484880cd8bed7e4842ecdd18ed81bf67ed19e03cVictim Addresses
0xf340bd3eb3e82994cff5b8c3493245edbce63436EthereumLoss Breakdown
Similar Incidents
CEXISWAP Proxy Takeover
33%Minimal-Proxy Pool Reinitializer Drain
32%dYdX Callback Approval Drain
32%Unibot Approval Drain
32%Public Curve Treasury Drain
32%0x7CAE Approved-Spender Drain
32%Root Cause Analysis
VRF Reconfiguration LINK Drain
1. Incident Overview TL;DR
An unprivileged attacker drained 162 LINK from proxy 0xf340bd3eb3e82994cff5b8c3493245edbce63436 in a single Ethereum mainnet transaction, then swapped the proceeds to ETH and returned the ETH to the attacker EOA. The exploit worked because the proxy exposed initVRF(address,address) as a live reconfiguration surface: any caller could replace the VRF target and LINK token addresses used by the later randomness-payment flow.
2. Key Background
The victim is a transparent proxy at 0xf340bd3eb3e82994cff5b8c3493245edbce63436 whose implementation resolves to 0xd92a9110beaf09115bc9628d8a296c2778041fe0 and whose admin resolves to 0x0c94ced619ac3f6fa1404fb485b6e17238d00f92. The implementation is unverified, so the analysis relies on saved runtime bytecode, disassembly, selector extraction, and transaction traces.
Before block 23232613, the proxy held 162.593046738967252304 LINK. The relevant pre-state proof shows owner() at block 23232612 returning 0xA8c92FE3E75E41E795d275654Df2572828834214, slot 0x28 already set to LINK 0x514910771af9ca656af840dff83e8264ecf986ca, and slot 0x29 set to 0xf0d54349addcf704f77ae15b96510dea15cb7952.
owner_pre_block=0xA8c92FE3E75E41E795d275654Df2572828834214
slot_0x28_pre=0x000000000000000000000000514910771af9ca656af840dff83e8264ecf986ca
slot_0x29_pre=0x000000000000000000000000f0d54349addcf704f77ae15b96510dea15cb7952
permissionless_eth_call_result="0x"
The saved selector extraction confirms the implementation exposes selector 0xd73f349b with address,address arguments and selector 0x607d60e6 with a uint256 argument.
3. Vulnerability Analysis & Root Cause Summary
The root cause is an authorization failure on initVRF(address,address). The implementation dispatcher routes selector 0xd73f349b into function entry 0x0eca. The saved disassembly for that function body shows only a nonpayable guard, ABI decoding, and two direct storage writes into slots 0x29 and 0x28. There is no CALLER opcode in that function body, so the path does not compare msg.sender against the owner, proxy admin, or any allowlist before rewriting the configuration. A separate saved eth_call from arbitrary EOA 0x1111111111111111111111111111111111111111 to initVRF(0x2222..., LINK) returned success (0x) instead of reverting, which matches the bytecode analysis. The protocol therefore trusted attacker-controlled VRF configuration in a later payment path and spent protocol-held LINK against that mutable target.
4. Detailed Root Cause Analysis
The decisive victim-side proof is the saved implementation disassembly and authorization note. The dispatcher matches 0xd73f349b and jumps to 0x0eca, where the code reaches two storage writes without any caller-based authorization check:
selector 0xd73f349b -> function entry 0x0eca
0x0eca..0x0f12:
nonpayable guard
decode arg0, arg1
SSTORE slot 0x29 <- arg0
SSTORE slot 0x28 <- arg1
return
The exploit transaction 0x103b4550a1a2bdb73e3cb5ea484880cd8bed7e4842ecdd18ed81bf67ed19e03c then exercised that path exactly as expected. The saved call tracer shows the attacker helper 0xd76c5305d0672ce5a2cdd1e8419b900410ea1d36 calling the proxy with 0xd73f349b..., and the proxy immediately delegating the same calldata to implementation 0xd92a9110beaf09115bc9628d8a296c2778041fe0.
{
"from": "0xd76c5305d0672ce5a2cdd1e8419b900410ea1d36",
"to": "0xf340bd3eb3e82994cff5b8c3493245edbce63436",
"input": "0xd73f349b000000000000000000000000d76c5305d0672ce5a2cdd1e8419b900410ea1d36000000000000000000000000514910771af9ca656af840dff83e8264ecf986ca"
}
The saved storage proof shows the concrete state effect of that call: slot 0x29 changed from the original coordinator-like address 0xf0d54349addcf704f77ae15b96510dea15cb7952 to the attacker helper, while slot 0x28 remained LINK. That is the invariant break: only the protocol operator should be able to control the endpoints that receive LINK-funded randomness payments.
After reconfiguration, the helper repeatedly called selector 0x607d60e6. Both the full trace and call tracer show that each invocation caused the proxy to execute LinkToken.transferAndCall(helper, 2e18, ...) and emit RequestRandomness(bytes32). This converted the protocol's internal randomness fee path into an attacker payout path.
LinkToken::transferAndCall(
0xD76C5305D0672CE5A2Cdd1e8419B900410ea1D36,
2000000000000000000,
0xaa77729d...
)
emit RequestRandomness(...)
Because the attacker helper computed the loop count from the proxy's LINK balance divided by the fixed 2 LINK request fee, the drain was deterministic. The trace then shows the helper approving the Uniswap V2 router, swapping exactly 162000000000000000000 LINK for 848179619158684245 WETH, unwrapping via WETH9.withdraw, and forwarding the ETH back to the attacker by SELFDESTRUCT.
5. Adversary Flow Analysis
The attacker EOA 0xda97a086fc74b20c88bd71e12e365027e9ec2d24 deployed transient helper contract 0xd76c5305d0672ce5a2cdd1e8419b900410ea1d36 inside the same transaction. The helper first called initVRF(address,address) on the victim proxy, passing itself as the VRF target and the canonical LINK token as the token address. That rewired the victim's randomness-payment configuration.
The helper then ran the drain loop by repeatedly calling selector 0x607d60e6(0). Each iteration caused the proxy to pay exactly 2 LINK to the helper through LINK's transferAndCall. With 81 iterations, the helper drained the full 162 LINK that was reachable in whole 2 LINK chunks, leaving the dust remainder below one request fee in the victim.
Finally, the helper swapped LINK to WETH on Uniswap V2 router 0x7a250d5630b4cf539739df2c5dacb4c659f2488d, withdrew WETH to ETH, and returned the ETH to the attacker EOA. The saved trace records the final payout as:
SELFDESTRUCT: contract: 0xd76c5305d0672ce5a2cdd1e8419b900410ea1d36
refund target: 0xda97a086fc74b20c88bd71e12e365027e9ec2d24
value 848179619158684245
6. Impact & Losses
The protocol lost 162000000000000000000 units of LINK, which is exactly 162 LINK at 18 decimals. The attacker EOA's native balance increased from 0.18 ETH to 1.025005252434861026 ETH, for a net gain of 845005252434861026 wei after fees, as shown in the saved balance diff.
This was an ACT opportunity: the attacker needed no privileged key material, no governance role, and no attacker-side predeployed infrastructure. A fresh EOA could deploy a helper contract, call the permissionless configuration function, and realize the profit in one transaction using public on-chain state.
7. References
- Seed transaction:
0x103b4550a1a2bdb73e3cb5ea484880cd8bed7e4842ecdd18ed81bf67ed19e03c - Victim proxy:
0xf340bd3eb3e82994cff5b8c3493245edbce63436 - Implementation:
0xd92a9110beaf09115bc9628d8a296c2778041fe0 - LINK token:
0x514910771af9ca656af840dff83e8264ecf986ca - Uniswap V2 router:
0x7a250d5630b4cf539739df2c5dacb4c659f2488d - Saved evidence:
artifacts/collector/seed/1/.../trace.cast.logartifacts/collector/seed/1/.../balance_diff.jsonartifacts/auditor/iter_1/proofs/implementation_disassembly.txtartifacts/auditor/iter_1/proofs/implementation_selectors.txtartifacts/auditor/iter_1/proofs/owner_and_storage_proofs.txtartifacts/auditor/iter_1/proofs/seed_tx_calltracer.jsonartifacts/auditor/iter_1/proofs/initvrf_authorization_analysis.txt