Vortex approveToken Drain
Exploit Transactions
Victim Addresses
0x2a2b195558cf89aa617979ce28880bbf7e17bc45Ethereum0x7b190a928aa76eece5cb3e0f6b3bdb24fcdd9b4fEthereumLoss Breakdown
Similar Incidents
V3Utils Arbitrary Call Drain
35%Dexible selfSwap allowance drain
34%NOON Pool Drain via Public transfer
34%WETH9 Balance Drain via Router 0x5697-1751 Using Victim
32%GPv2Settlement allowance leak lets router drain WETH and USDC
32%WBTC Drain via Insecure Router transferFrom Path
32%Root Cause Analysis
Vortex approveToken Drain
1. Incident Overview TL;DR
Vortex lost the full idle USDT balance of DEPUSDT (0x7b190a928aa76eece5cb3e0f6b3bdb24fcdd9b4f) and the full idle USDC balance of LEVUSDC (0x2a2b195558cf89aa617979ce28880bbf7e17bc45) in two adversary-crafted Ethereum transactions: 0xf0a13b445674094c455de9e947a25bade75cac9f5176695fca418898ea25742f at block 17484162 and 0x800a5b3178f680feebb81af69bd3dff791b886d4ce31615e601f2bb1f543bb2e at block 17484168. The attacker EOA 0x7021c1b142eb634fa0749cda270c7aff74dc3b7f first deployed helper contract 0xca813e8ba2fd072bde40dd2264bc3774ff685d9c in transaction 0x47bc8617dd53c157d6eaf7c5f1fc0f36138a1e5547c276950fb2e51dbfd7b1fb, then used that helper to call into the two Vortex markets.
The root cause is a direct access-control failure. Both market implementations inherit CurveSwap, whose approveToken(address token, address spender, uint _amount) function is public and performs an unrestricted token approval from the proxy's own balance. Because the live proxy ABIs expose that selector, any unprivileged caller can grant itself or a helper allowance over protocol-held USDT or USDC and then immediately drain those balances with transferFrom.
2. Key Background
Vortex deployed DEPUSDT and LEVUSDC as upgradeable market proxies on Ethereum mainnet. DEPUSDT is backed by USDT and delegates to DepErc20 implementation 0x94290106d2a32bc89be9f1c3a3f3394f64578aa6; LEVUSDC is backed by USDC and delegates to LevErc20 implementation 0x27c55a6bd85e79c70c9b2caa003d55a2ece01565.
Both implementations inherit the same CurveSwap utility contract. That inheritance matters because Solidity exposes inherited public functions through the proxy ABI, even when the function was originally intended as an internal helper for protocol-controlled swaps. Historical RPC validation at block 17484161 confirms the exposure: approveToken(USDC, 0x1111..., 1) on LEVUSDC and approveToken(USDT, 0x1111..., 1) on DEPUSDT both return true for an arbitrary caller.
Immediately before the exploit sequence, the public pre-state was favorable to an attacker:
DEPUSDTheld69961509697raw USDT units.LEVUSDCheld36142023929raw USDC units.- The attacker EOA held
0USDT and0USDC.
That is sufficient for an ACT opportunity because no privileged key, governance action, oracle manipulation, or hidden off-chain input is required. The only prerequisites are visible token balances and a permissionless call path to approveToken.
3. Vulnerability Analysis & Root Cause Summary
This is an ATTACK-class ACT incident caused by an unrestricted approval primitive on protocol-owned assets. The vulnerable code lives in CurveSwap, which both Vortex market implementations inherit without wrapping the function in authorization checks. The function accepts attacker-controlled token, spender, and amount parameters and directly executes an ERC20 approval from the proxy itself. Once that approval exists, any standard ERC20 with normal transferFrom semantics allows the approved spender to pull the market's balance out of the proxy. The traces show exactly that pattern for both markets: approval first, balance read second, full-balance transfer third. The broken invariant is straightforward: only trusted protocol flows should ever be able to create or change token allowances from balances owned by DEPUSDT or LEVUSDC, which is also a failure of least privilege and inheritance-surface review for administrative asset-management helpers.
4. Detailed Root Cause Analysis
4.1 Vulnerable Code Path
Source excerpt from Vortex's verified CurveSwap implementation inherited by both DepToken and LevToken:
function approveToken(address token, address spender, uint _amount) public returns (bool) {
IERC20(token).safeApprove(spender, _amount);
return true;
}
This function is the code-level breakpoint. It mutates an ERC20 allowance from the calling Vortex market to an arbitrary spender without checking msg.sender, approved token set, approved spender set, or any protocol role. Because DEPUSDT and LEVUSDC are proxies that delegate to implementations inheriting CurveSwap, the selector is reachable on the live market addresses themselves.
Historical chain-state checks independently confirm the reachability condition at the exploit pre-state. At block 17484161, calling approveToken through each proxy with arbitrary parameters returns true, showing that the function was not restricted to governance or internal-only flows.
4.2 Exploit Mechanism
The attacker first created helper contract 0xca813e8ba2fd072bde40dd2264bc3774ff685d9c, deployed by EOA 0x7021c1b142eb634fa0749cda270c7aff74dc3b7f in block 17484125. Contract-creation records link the helper directly to that EOA, and the helper's runtime behavior matches the later traces: call approveToken, read the victim token balance, then call transferFrom to move the full balance to the creator EOA.
The DEPUSDT drain trace shows the exploit sequence directly:
0xca813e8b...::3232a1fd(DEPUSDT, USDT)
0x7b190a92...::approveToken(USDT, 0xca813e8b..., type(uint256).max)
DepErc20::approveToken(...) [delegatecall]
TetherToken::approve(0xca813e8b..., type(uint256).max)
TetherToken::balanceOf(0x7b190a92...) -> 69961509697
TetherToken::transferFrom(0x7b190a92..., 0x7021c1b1..., 69961509697)
The LEVUSDC drain is the same pattern, just against the USDC-backed market:
0xca813e8b...::3232a1fd(LEVUSDC, USDC)
0x2a2b1955...::approveToken(USDC, 0xca813e8b..., type(uint256).max)
LevErc20::approveToken(...) [delegatecall]
FiatTokenV2_1::approve(0xca813e8b..., type(uint256).max)
FiatTokenV2_1::balanceOf(0x2a2b1955...) -> 36142023929
FiatTokenV2_1::transferFrom(0x2a2b1955..., 0x7021c1b1..., 36142023929)
Nothing else is needed. The attacker does not need to touch Vortex accounting logic, borrow logic, or admin-only functions. The public approval primitive alone is enough to convert protocol custody into attacker custody.
4.3 ACT Framing and Safety Invariant
The ACT pre-state is Ethereum mainnet at block 17484161, immediately before block 17484162. In that state:
DEPUSDTstill holds positive USDT.LEVUSDCstill holds positive USDC.- The inherited
approveTokenselector is callable through both proxies. - USDT and USDC both honor standard
approveandtransferFrombehavior.
The deterministic ACT success predicate is the "Idle Treasury Drain" oracle: an unprivileged caller can reduce both positive victim balances to zero while transferring those same balances to an attacker-controlled recipient. The safety invariant is: only trusted Vortex flows should be able to authorize third-party spending of market-owned assets. The exploit violates that invariant at the exact moment approveToken writes the allowance to attacker-controlled spender state. After that breakpoint, the subsequent transferFrom calls are ordinary ERC20 mechanics.
5. Adversary Flow Analysis
- In transaction
0x47bc8617dd53c157d6eaf7c5f1fc0f36138a1e5547c276950fb2e51dbfd7b1fb, attacker EOA0x7021c1b142eb634fa0749cda270c7aff74dc3b7fdeploys helper contract0xca813e8ba2fd072bde40dd2264bc3774ff685d9c. - In transaction
0xf0a13b445674094c455de9e947a25bade75cac9f5176695fca418898ea25742f, the attacker calls the helper withDEPUSDTand USDT as parameters. - The helper invokes
DEPUSDT.approveToken(USDT, helper, type(uint256).max), then readsDEPUSDT's USDT balance, then transfers the full69961509697raw units to the attacker EOA. - In transaction
0x800a5b3178f680feebb81af69bd3dff791b886d4ce31615e601f2bb1f543bb2e, the attacker repeats the same steps againstLEVUSDCand USDC, pulling the full36142023929raw units to the same EOA. - Across the two transactions the attacker pays
8549320785947295wei in gas and ends with all stolen stablecoins in the originating EOA, while both victim proxy balances fall to zero.
The adversary-controlled account cluster is therefore deterministic:
- EOA
0x7021c1b142eb634fa0749cda270c7aff74dc3b7f: helper deployer, sender of both exploit transactions, final recipient of both stablecoin transfers. - Helper
0xca813e8ba2fd072bde40dd2264bc3774ff685d9c: one-purpose wrapper that automatesapproveTokenplustransferFrom.
6. Impact & Losses
The exploit drained all idle stablecoin inventory that was resident in the two Vortex market proxies at the pre-state:
DEPUSDT:69961509697raw USDT units, equal to69,961.509697USDT.LEVUSDC:36142023929raw USDC units, equal to36,142.023929USDC.
The total deterministic loss is therefore 106,103.533626 units across the two stablecoins, before considering the attacker's gas cost. The victim scope is limited to the exposed balances on those two proxy markets at the chosen block, but the code path itself is broader: any Vortex market inheriting the same public helper and holding compatible ERC20 balances would be vulnerable to the same technique.
7. References
- Ethereum transaction
0x47bc8617dd53c157d6eaf7c5f1fc0f36138a1e5547c276950fb2e51dbfd7b1fb: helper contract deployment for0xca813e8ba2fd072bde40dd2264bc3774ff685d9c. - Ethereum transaction
0xf0a13b445674094c455de9e947a25bade75cac9f5176695fca418898ea25742f: DEPUSDT USDT drain trace showingapproveToken,balanceOf, andtransferFrom. - Ethereum transaction
0x800a5b3178f680feebb81af69bd3dff791b886d4ce31615e601f2bb1f543bb2e: LEVUSDC USDC drain trace showing the same sequence. - Verified Vortex implementation source for
DepErc20at0x94290106d2a32bc89be9f1c3a3f3394f64578aa6, which inheritsCurveSwap. - Verified Vortex implementation source for
LevErc20at0x27c55a6bd85e79c70c9b2caa003d55a2ece01565, which inheritsCurveSwap. - Independent historical RPC calls at block
17484161confirmingapproveTokenis externally callable on both live proxies.