# Unused ERC20 tokens are not refunded, and can be stolen by attacker

SingleTokenJoinV2 only refunds leftover inputToken, leaving intermediate basket tokens stranded in the contract where an attacker can steal them via fake swap/outputBasket.

- Payout: Audit contest
- Severity: high
- Program: Amun
- Found by: WatchPug
- Disclosed: 2023-03-07
- Write-up quality: 7/10
- Original: https://github.com/code-423n4/2021-12-amun-findings/issues/201
- Library page: https://bugbountylibrary.com/r/c4%3A2021-12-amun%3AH-01
- Tags: area:web3 domain:web3 impact:funds-loss kind:contest platform:code4rena sev:high tech:evm tech:solidity technique:source-review vuln:access-control vuln:business-logic

**Target:** SingleTokenJoinV2 / BasketFacet join flow (Solidity contracts)
**Root cause:** joinTokenSingle swaps inputToken into multiple basket underlying tokens but only refunds the leftover inputToken balance; any excess underlying tokens remain in the contract. joinPool only pulls the exact required amounts, so intermediate leftovers accumulate.
**Steps:**
1. User calls `joinTokenSingle(_joinTokenStruct)` with inputToken/inputAmount
2. Contract pulls inputAmount via `safeTransferFrom`
3. `_joinTokenSingle` swaps into basket underlying tokens
4. `BasketFacet.joinPool` pulls only `balance*tokenAmount/totalSupply` per token, leaving excess underlying tokens in SingleTokenJoinV2
5. Only `inputToken.balanceOf(address(this))` is refunded; leftover underlying tokens stay in contract
6. Attacker calls `joinTokenSingle()` with attacker-controlled fake `outputBasket` and `swap.exchange` contracts to route the stranded tokens to themselves
**How found:** Code review of joinTokenSingle refund logic: noticed only inputToken balance is refunded while joinPool pulls variable amounts based on current pool balances, implying intermediate token leftovers; then observed those leftovers are claimable via attacker-controlled swap/outputBasket params.
**Exploitation:** Attacker supplies malicious `outputBasket` and `swap.exchange` contract addresses in the JoinTokenStruct so the swap step sends the contract's existing underlying token balances to attacker-controlled addresses, draining accumulated leftovers.
**Insight:** In multi-token join/swap contracts, check that every intermediate token acquired during the swap is either fully consumed or refunded — and that swap/router addresses are not caller-supplied, otherwise stranded balances become a drain primitive.
