All incidents

Dexible selfSwap allowance drain

Share
Feb 17, 2023 04:20 UTCAttackLoss: 17,960,937.5 TRUPending manual check1 exploit txWindow: Atomic
Estimated Impact
17,960,937.5 TRU
Label
Attack
Exploit Tx
1
Addresses
2
Attack Window
Atomic
Feb 17, 2023 04:20 UTC → Feb 17, 2023 04:20 UTC

Exploit Transactions

TX 1Ethereum
0x138daa4cbeaa3db42eefcec26e234fc2c89a4aa17d6b1870fc460b2856fd11a6
Feb 17, 2023 04:20 UTCExplorer

Victim Addresses

0xde62e1b0edaa55aac5ffbe21984d321706418024Ethereum
0x58f5f0684c381fcfc203d77b2bba468ebb29b098Ethereum

Loss Breakdown

17,960,937.5TRU

Similar Incidents

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 for routeAmount.
  • routeAmount: the amount Dexible uses for its initial token pull and approval.
  • routerData: opaque calldata forwarded to router.

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 0xde62e1b0edaa55aac5ffbe21984d321706418024 and implementation 0x33e690aea97e4ef25f0d140f1bf044d663091daf
  • SwapHandler.preCheck and SwapHandler.fill
  • SwapTypes.RouterRequest, which exposes attacker-controlled router, spender, routeAmount, and routerData
  • 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:

  1. Dexible accepted a route whose router was the TRU token contract itself.
  2. Dexible pulled zero TRU input for the malicious route because routeAmount.amount = 0.
  3. Dexible executed the attacker-crafted transferFrom(victim, attacker, allowance) call from Dexible's own allowance-bearing context.
  4. 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 selfSwap call 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:

  1. In transaction 0xeb3a560927b118149f68acb9b11eeb93fcc91d7f98a315117974eecdcd42e206 at block 16645940, EOA 0x684083f312ac50f538cc4b634d85a2feafaab77a created helper contract 0x194fc30f9eeba9ad673413629b47fc00e71d90df.
  2. In transaction 0x1139f3c3e374e785c7be5413226d0be32bc21d477ee8d0ff0b38f4ab93ff1df2 at block 16645951, the same EOA transferred 1000000 USDC base units to the helper. That funding let the helper pay Dexible's fee path during the exploit.
  3. In the seed exploit transaction 0x138daa4cbeaa3db42eefcec26e234fc2c89a4aa17d6b1870fc460b2856fd11a6, the helper called Dexible's public selfSwap entrypoint and supplied a route that was not a swap at all. The route directly invoked TRU transferFrom against the victim's pre-existing Dexible allowance.
  4. Dexible then distributed small USDC fee payments out of the helper balance: 5762 USDC units to 0x5db6e1b7ce743a2d49b2546b3ebe17132e0ab04d and 5761 USDC units to community vault 0xeb890541049ccd965d3dd4a3ec1ad368fd4b26a4. 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: -8649007906159894 wei

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

  1. Exploit transaction: https://etherscan.io/tx/0x138daa4cbeaa3db42eefcec26e234fc2c89a4aa17d6b1870fc460b2856fd11a6
  2. Helper deployment transaction: https://etherscan.io/tx/0xeb3a560927b118149f68acb9b11eeb93fcc91d7f98a315117974eecdcd42e206
  3. Helper funding transaction: https://etherscan.io/tx/0x1139f3c3e374e785c7be5413226d0be32bc21d477ee8d0ff0b38f4ab93ff1df2
  4. Dexible proxy source: https://etherscan.io/address/0xde62e1b0edaa55aac5ffbe21984d321706418024#code
  5. Dexible implementation source: https://etherscan.io/address/0x33e690aea97e4ef25f0d140f1bf044d663091daf#code
  6. Community vault source: https://etherscan.io/address/0xeb890541049ccd965d3dd4a3ec1ad368fd4b26a4#code
  7. Seed trace artifact: /workspace/session/artifacts/collector/seed/1/0x138daa4cbeaa3db42eefcec26e234fc2c89a4aa17d6b1870fc460b2856fd11a6/trace.cast.log
  8. Seed balance diff artifact: /workspace/session/artifacts/collector/seed/1/0x138daa4cbeaa3db42eefcec26e234fc2c89a4aa17d6b1870fc460b2856fd11a6/balance_diff.json
  9. TRU token source collected in the seed artifacts at 0x095527f5bea113e9575b662c5ba01d990a280f2f