Dexible selfSwap allowance drain
Exploit Transactions
0x138daa4cbeaa3db42eefcec26e234fc2c89a4aa17d6b1870fc460b2856fd11a6Victim Addresses
0xde62e1b0edaa55aac5ffbe21984d321706418024Ethereum0x58f5f0684c381fcfc203d77b2bba468ebb29b098EthereumLoss Breakdown
Similar Incidents
Public SwapGuard envelope enabled arbitrary transferFrom drain of CoW Settlement DAI allowance
37%V3Utils Arbitrary Call Drain
37%GPv2Settlement allowance leak lets router drain WETH and USDC
36%LiFi router allowance-drain exploit steals approved holder tokens
35%Vortex approveToken Drain
34%TecraCoin TcrToken burnFrom Allowance Bug Exploits
34%Root Cause Analysis
Dexible selfSwap allowance drain
1. Incident Overview TL;DR
Dexible was exploited on Ethereum mainnet in transaction 0x138daa4cbeaa3db42eefcec26e234fc2c89a4aa17d6b1870fc460b2856fd11a6 at block 16646023. The attacker cluster centered on EOA 0x684083f312ac50f538cc4b634d85a2feafaab77a used helper contract 0x194fc30f9eeba9ad673413629b47fc00e71d90df to call Dexible.selfSwap on proxy 0xde62e1b0edaa55aac5ffbe21984d321706418024. Instead of performing a real swap, the route pointed directly at the TRU token contract 0x4c19596f5aaff459fa38b0f7ed92f11ae6543784 and supplied calldata for transferFrom(victim, attacker, amount).
The victim 0x58f5f0684c381fcfc203d77b2bba468ebb29b098 had previously approved Dexible to spend TRU. Dexible executed the attacker-supplied call from its own approved-spender context, so the token contract honored Dexible's allowance and transferred 1796093750000000 raw TRU units, or 17,960,937.5 TRU, directly to the attacker EOA. The root cause is a confused-deputy arbitrary-call flaw: Dexible trusts attacker-controlled router, spender, routeAmount, and routerData fields in selfSwap / fill, and it only enforces declarative fee and output metadata instead of actual swap semantics.
2. Key Background
Dexible is a DEX aggregation contract that lets a caller submit a SelfSwap request containing a fee token, input and output token declarations, and an array of RouterRequest objects. Each RouterRequest includes four attacker-controlled fields:
router: the contract Dexible will call.spender: the address Dexible will approve forrouteAmount.routeAmount: the amount Dexible uses for its initial token pull and approval.routerData: opaque calldata forwarded torouter.
The verified source for Dexible's implementation at 0x33e690aea97e4ef25f0d140f1bf044d663091daf exposes the relevant types and comments:
struct RouterRequest {
//router contract that handles the specific route data
address router;
//any spend allowance approval required
address spender;
//the amount to send to the router
TokenTypes.TokenAmount routeAmount;
//the data to use for calling the router
bytes routerData;
}
The same source comments that only approved routers should execute successfully, but the implementation contains no router allowlist. Search over the verified source finds whitelisting only for relay wallets, not for route targets.
Victims had previously granted Dexible ERC20 allowances for legitimate aggregator use. That approval model is safe only if Dexible constrains what it is allowed to do with those allowances. Here it did not: any public caller can cause Dexible to spend an approved victim's tokens on arbitrary calldata.
3. Vulnerability Analysis & Root Cause Summary
This is an ATTACK-class bug, not a pure MEV opportunity. Dexible exposes an arbitrary-call surface inside a contract that already has third-party ERC20 allowances. selfSwap copies untrusted route fields into a SwapRequest, and preCheck only transfers request.routes[0].routeAmount.amount from the caller. That means the attacker can set the first route amount to zero and avoid supplying the declared input asset.
The code-level breakpoint is in SwapHandler.fill, where Dexible approves the route spender and then performs a raw call to the attacker-chosen router address. It does not verify that the route target is an authorized swap adapter, that routerData encodes a legitimate swap, or that the actual asset movements correspond to the declared input and output amounts. Post-call, Dexible only checks whether the observed output is at least request.tokenOut.amount, which the attacker can also set to zero.
The invariant that should hold is straightforward: Dexible must only spend the requester's own assets to execute trusted swap logic and settle the declared output. The exploit breaks that invariant because Dexible spends a third party's approval on the TRU token contract itself and transfers the victim's balance directly to the attacker. Any unprivileged adversary can reproduce the sequence so long as a victim has a live Dexible allowance and sufficient balance.
Affected components and violated principles from the validated root cause are:
- Dexible proxy
0xde62e1b0edaa55aac5ffbe21984d321706418024and implementation0x33e690aea97e4ef25f0d140f1bf044d663091daf SwapHandler.preCheckandSwapHandler.fillSwapTypes.RouterRequest, which exposes attacker-controlledrouter,spender,routeAmount, androuterData- never execute arbitrary user-controlled external calls from a contract that holds third-party approvals
- never trust declarative trade metadata without reconciling it to actual token movement
- an aggregator approval is permission for constrained swap execution, not arbitrary calldata execution
4. Detailed Root Cause Analysis
The verified Dexible implementation shows the exact flaw:
function fill(SwapTypes.SwapRequest calldata request, SwapMeta memory meta)
external
onlySelf
returns (SwapMeta memory)
{
preCheck(request, meta);
meta.outAmount = request.tokenOut.token.balanceOf(address(this));
for (uint i = 0; i < request.routes.length; ++i) {
SwapTypes.RouterRequest calldata rr = request.routes[i];
IERC20(rr.routeAmount.token).safeApprove(rr.spender, rr.routeAmount.amount);
(bool s, ) = rr.router.call(rr.routerData);
if (!s) revert("Failed to swap");
}
uint out = request.tokenOut.token.balanceOf(address(this));
meta.outAmount = meta.outAmount < out ? out - meta.outAmount : 0;
require(meta.outAmount >= request.tokenOut.amount, "Insufficient output generated");
return meta;
}
function preCheck(SwapTypes.SwapRequest calldata request, SwapMeta memory meta) internal {
...
request.tokenIn.token.safeTransferFrom(
request.executionRequest.requester,
address(this),
request.routes[0].routeAmount.amount
);
}
selfSwap is the public entrypoint that makes this reachable by any caller:
function selfSwap(SwapTypes.SelfSwap calldata request) external notPaused {
SwapTypes.SwapRequest memory swapReq = SwapTypes.SwapRequest({
executionRequest: ExecutionTypes.ExecutionRequest({
fee: ExecutionTypes.FeeDetails({
feeToken: request.feeToken,
affiliate: address(0),
affiliatePortion: 0
}),
requester: msg.sender
}),
tokenIn: request.tokenIn,
tokenOut: request.tokenOut,
routes: request.routes
});
details = this.fill(swapReq, details);
postFill(swapReq, details, true);
}
The seed trace for transaction 0x138daa4cbeaa3db42eefcec26e234fc2c89a4aa17d6b1870fc460b2856fd11a6 shows the exploit path exactly:
0xDE62E1b0edAa55aAc5ffBE21984D321706418024::selfSwap(
feeToken = USDC,
tokenIn.amount = 14403789,
tokenOut.amount = 0,
routes[0] = (
router = TRU,
spender = Dexible,
routeAmount.amount = 0,
routerData = transferFrom(
0x58f5f0684c381fcfc203d77b2bba468ebb29b098,
0x684083f312ac50f538cc4b634d85a2feafaab77a,
1796093750000000
)
)
)
...
TRU::transferFrom(victim, attacker, 1796093750000000)
emit Transfer(from: victim, to: attacker, value: 1796093750000000)
emit Approval(owner: victim, spender: Dexible, value: 0)
This trace proves four critical facts:
- Dexible accepted a route whose
routerwas the TRU token contract itself. - Dexible pulled zero TRU input for the malicious route because
routeAmount.amount = 0. - Dexible executed the attacker-crafted
transferFrom(victim, attacker, allowance)call from Dexible's own allowance-bearing context. - The victim allowance to Dexible was consumed to zero in the same call.
The balance diff for the same transaction independently confirms the asset movement:
{
"token": "0x4c19596f5aaff459fa38b0f7ed92f11ae6543784",
"holder": "0x58f5f0684c381fcfc203d77b2bba468ebb29b098",
"delta": "-1796093750000000"
}
{
"token": "0x4c19596f5aaff459fa38b0f7ed92f11ae6543784",
"holder": "0x684083f312ac50f538cc4b634d85a2feafaab77a",
"delta": "1796093750000000"
}
That same trace segment also shows Dexible's own TRU balance remaining at zero before and after the malicious route. This matches the non-monetary exploit predicate from the validated root cause: Dexible acted only as a confused deputy that consumed the victim's live approval and delivered the approved TRU directly to the attacker instead of ever taking custody itself.
No privileged access is required. The ACT preconditions are only:
- a victim has a positive ERC20 allowance to Dexible,
- the victim balance covers the approved amount,
- the attacker can submit a public
selfSwapcall with attacker-controlled route fields.
Under those conditions, Dexible becomes an arbitrary approved spender for the attacker.
The relevant ACT pre-state is Ethereum mainnet immediately before the exploit transaction in block 16646023, with Dexible deployed and callable, the victim's TRU balance and allowance still live, and the attacker helper already funded.
5. Adversary Flow Analysis
The attacker lifecycle is short and fully observable on-chain:
- In transaction
0xeb3a560927b118149f68acb9b11eeb93fcc91d7f98a315117974eecdcd42e206at block16645940, EOA0x684083f312ac50f538cc4b634d85a2feafaab77acreated helper contract0x194fc30f9eeba9ad673413629b47fc00e71d90df. - In transaction
0x1139f3c3e374e785c7be5413226d0be32bc21d477ee8d0ff0b38f4ab93ff1df2at block16645951, the same EOA transferred1000000USDC base units to the helper. That funding let the helper pay Dexible's fee path during the exploit. - In the seed exploit transaction
0x138daa4cbeaa3db42eefcec26e234fc2c89a4aa17d6b1870fc460b2856fd11a6, the helper called Dexible's publicselfSwapentrypoint and supplied a route that was not a swap at all. The route directly invoked TRUtransferFromagainst the victim's pre-existing Dexible allowance. - Dexible then distributed small USDC fee payments out of the helper balance:
5762USDC units to0x5db6e1b7ce743a2d49b2546b3ebe17132e0ab04dand5761USDC units to community vault0xeb890541049ccd965d3dd4a3ec1ad368fd4b26a4. The helper also indirectly paid gas, while the attacker EOA received the stolen TRU directly.
The helper contract is operational detail, not a privilege boundary. The strategy is still ACT because any unprivileged EOA or freshly deployed helper contract can submit the same selfSwap structure with a new recipient address and realize the same unauthorized transfer.
6. Impact & Losses
The directly observed seed loss is 1796093750000000 raw TRU units, which equals 17,960,937.5 TRU at 8 decimals. The victim address in the seed transaction is 0x58f5f0684c381fcfc203d77b2bba468ebb29b098.
The seed balance diff also shows the trade-off around that profit:
- attacker EOA TRU delta:
+1796093750000000 - victim TRU delta:
-1796093750000000 - helper USDC delta:
-11523 - attacker EOA native delta:
-8649007906159894wei
Measured in the root cause's reference asset, the success predicate is therefore a positive TRU gain of 1796093750000000 raw units for the adversary cluster.
The protocol-wide impact is broader than the single observed loss. Dexible's flaw exposed any token holder with an active Dexible allowance and sufficient balance, because the exploit requires no privileged role, no stolen keys, and no attacker-specific artifacts. Until the protocol was paused or users revoked approvals, Dexible operated as a public arbitrary-call spender against approved users.
7. References
- Exploit transaction:
https://etherscan.io/tx/0x138daa4cbeaa3db42eefcec26e234fc2c89a4aa17d6b1870fc460b2856fd11a6 - Helper deployment transaction:
https://etherscan.io/tx/0xeb3a560927b118149f68acb9b11eeb93fcc91d7f98a315117974eecdcd42e206 - Helper funding transaction:
https://etherscan.io/tx/0x1139f3c3e374e785c7be5413226d0be32bc21d477ee8d0ff0b38f4ab93ff1df2 - Dexible proxy source:
https://etherscan.io/address/0xde62e1b0edaa55aac5ffbe21984d321706418024#code - Dexible implementation source:
https://etherscan.io/address/0x33e690aea97e4ef25f0d140f1bf044d663091daf#code - Community vault source:
https://etherscan.io/address/0xeb890541049ccd965d3dd4a3ec1ad368fd4b26a4#code - Seed trace artifact:
/workspace/session/artifacts/collector/seed/1/0x138daa4cbeaa3db42eefcec26e234fc2c89a4aa17d6b1870fc460b2856fd11a6/trace.cast.log - Seed balance diff artifact:
/workspace/session/artifacts/collector/seed/1/0x138daa4cbeaa3db42eefcec26e234fc2c89a4aa17d6b1870fc460b2856fd11a6/balance_diff.json - TRU token source collected in the seed artifacts at
0x095527f5bea113e9575b662c5ba01d990a280f2f