• Stars
    star
    137
  • Rank 266,121 (Top 6 %)
  • Language
    Python
  • Created almost 4 years ago
  • Updated almost 4 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

evil jar attack technical post-mortem

Evil Jar Technical Post-mortem

Authors

@banteg @emilianobonassi @lehnberg @samczsun @vasa-develop @bneiluj

Summary

  • On Saturday November 21 2020, an attacker drained 19 million DAI from pickle.finance's pDAI Jar.
  • Taking advantage of multiple flaws in the system, including issues with the Jar swap and Jar convert logic, the attacker was able to craft a sophisticated exploit to carry out the heist.
  • Dubbed "Evil Jar", the attack has been reverse-engineered successfully with details shared below.

Background

Pickle Jars are forked versions of Yearn Vaults v1 with modifications. The Jars are controlled by a Controller contract, its latest version enabling direct swaps between Jars. It was this added swap functionality that was leveraged together with multiple design flaws in order to execute the attack.

Details of vulnerability

Pickle ControllerV4's swapExactJarForJar() function can be used to drain non-Jar tokens from Strategies or tokens that end up in the Controller.

// Function to swap between jars
function swapExactJarForJar(
    address _fromJar, // From which Jar
    address _toJar, // To which Jar
    uint256 _fromJarAmount, // How much jar tokens to swap
    uint256 _toJarMinAmount, // How much jar tokens you'd like at a minimum
    address payable[] calldata _targets,
    bytes[] calldata _data
) external returns (uint256)

An attacker can craft an EvilJar contract with a part of the exploit payload, as well as a FakeUnderlying contract, which holds another part of the payload.

EvilJar is passed as both _fromJar and _toJar arguments. Since Controller doesn't verify if the Jar contract is legit, it's enough to implement token, getRatio, decimals, transfer, transferFrom, approve, allowance, balanceOf, withdraw and deposit functions to make it work with the Jar.

The deposit() function holds the payload and transfers the tokens from the Controller to the attacker. It could look like this:

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

It would be called here, with _toJar being the EvilJar and _toBal being the token balance in the Controller:

// Deposit into new Jar
uint256 _toBal = IERC20(_toJarToken).balanceOf(address(this));
IERC20(_toJarToken).safeApprove(_toJar, 0);
IERC20(_toJarToken).safeApprove(_toJar, _toBal);
IJar(_toJar).deposit(_toBal);  // call to EvilJar

The above makes it possible to drain funds from the Controller. For this to have effect, there needs to be funds there to drain.

A user can provide a list of arbitrary _targets and _data. Each target needs to be marked as approvedJarConverter:

for (uint256 i = 0; i < _targets.length; i++) {
    require(_targets[i] != address(0), "!converter");
    require(approvedJarConverters[_targets[i]], "!converter");
}

At the time of this writing, there are two Jar Converters approved:

  1. UniswapV2ProxyLogic
  2. CurveProxyLogic, this one is vulnerable and allows code injection

After withdrawing funds from the fabricated EvilJar, the controller delegate calls the provided Converters with the data provided by the attacker. This means an attacker can call the code from their contract in the context of the Controller.

CurveProxyLogic has the following function which can be used to inject an arbitrary call:

function add_liquidity(
    address curve,
    bytes4 curveFunctionSig,
    uint256 curvePoolSize,
    uint256 curveUnderlyingIndex,
    address underlying
) public {
    uint256 underlyingAmount = IERC20(underlying).balanceOf(address(this));  // call to FakeUnderlying
    uint256[] memory liquidity = new uint256[](curvePoolSize);
    liquidity[curveUnderlyingIndex] = underlyingAmount;
    bytes memory callData = abi.encodePacked(
        curveFunctionSig,
        liquidity,  // our injected value
        uint256(0)
    );
    ...
    (bool success, ) = curve.call(callData);
}

By supplying a target address as curve, a function 4-byte identifier as curveFunctionSig and FakeUnderlying as underlying, an attacker can make the Controller call a function in the target Strategy, with a limitation of that function requiring zero or one argument.

A FakeUnderlying contract needs to implement balanceOf, allowance and approve to work. The exploit payload goes into the balanceOf, which sets the first argument of the function to be called:

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

In this case, calling Strategy.withdraw(address), seizes dust that is sent to the Controller.

As a preventive measure, the function disallows withdrawing the want token, which is the deposit token of the Jar. So in the case of the pDAI Jar, it would not allow stealing DAI this way. However, the pDAI Jar's Strategy holds cDAI, a tokenized Compound deposit, and considers this incorrectly to be dust.

It was the combination of the above flaws that allowed the attacker to proceed with the exploit.

Details of exploit

Reverse-engineered Exploit

Our simplified and more efficient reproduction of the exploit is published at https://github.com/banteg/evil-jar. Note that it is different from what was actually used in the attack.

Original Exploit

Exploit transaction trace.

  1. Deploy two Evil Jars

  2. Get the amount available to withdraw from StrategyCmpdDaiV2 StrategyCmpdDaiV2.getSuppliedUnleveraged() => 19728769153362174946836922

  3. Invoke ControllerV4.swapExactJarForJar() passing the Evil Jars and the amount retrieved in the previous step.

  4. ControllerV4.swapExactJarForJar() doesn't check the Jars and calls them, withdrawing from StrategyCmpDAIV2 using StrategyCmpDAIV2.withdrawForSwap() which ultimately usesStrategyCmpDAIV2.deleverageToMin(). This transfers 19M DAI to pDAI. We are still in Pickle Contracts, in this part of the attack Evil Jars were used just to put the funds to pDAI.

  5. Call pDAI.earn() 3 times. This invokes a Compound deposit via StrategyCmpDAIV2.deposit(), leading to the contract receiving cDAI. StrategyCmpdDAIV2 now has an equivalent of 19M in cDAI.

  6. Deploy 3 more evil contracts, the first one being the equivalent of FakeUnderlying in our replicated exploit and the other two Evil Jars.

  7. Invoke ControllerV4.swapExactJarForJar() passing the Evil Jars, no amount and a CurveProxyLogic as target with a crafted data which allowed an injection to call the equivalent FakeUnderlying.

  8. ControllerV4 delegate calls CurveProxyLogic.add_liquidity() passing StrategyCmpDAIV2 and a crafted signature which leads to withdrawal of cDAI and transferring them to ControllerV4.

  9. The funds (in cDAI) are now in the Controller, it calls the EvilJar.deposit() which transfer the funds to the attacker smart contract.

  10. The attacker smart contract redeems cDAI for DAI from Compound and transfers DAI to the attacker EOA.

Details of fix

The first part of mitigating the problem was to prevent further deposits into the PickleJar. For this, the Pickle Finance team called setMin(0) on the DAI PickleJar via the governance multisig.

As described above, the offending logic that allows arbitrary code injection is located in CurveProxyLogic, which is a Converter approved for use within the Controller. In order to revoke this Converter, this function in the Controller must be called:

function revokeJarConverter(address _converter) public {
    require(msg.sender == governance, "!governance");
    approvedJarConverters[_converter] = false;
}

At the time, the governance role was set to a 12-hour Timelock so it was decided that the governance multisig address would be set as the new governance role so that the Pickle Finance team could invoke this function without waiting for a Timelock. This transaction was queued and executed by 2020-11-22 03:15 PM (UTC), granting the Pickle Finance team the ability to revoke CurveProxyLogic from use by calling revokeJarConverter().

While this removes a key piece of the exploit, there are still further issues of concern as explained in the sections above. The Pickle Finance team will continue to work in the coming days and weeks to fix these vulnerabilities.

Timeline of events

References

More Repositories

1

brownie-safe

gnosis safe tx builder
Python
268
star
2

multicall.py

aggregate results of multiple smart contract calls into one
Python
245
star
3

storage-layout

readable evm state diffs, enumerate full contract storage
Python
134
star
4

cryogen

helps you preserve the ethereum dataset fresh, fast and small
Python
115
star
5

seed-liquidity

Pool funds to bootstrap a Uniswap pair
Python
77
star
6

vyper-reentrancy

Vyper
63
star
7

erigon-kv

python bindings for erigon kv gprc api
Python
54
star
8

disperse-research

research behind disperse protocol
TeX
52
star
9

fast

fast.com cli speedtest
Python
49
star
10

yfi-buyer

buys yfi at current price
Python
42
star
11

ape-llamapay

ape sdk for llamapay
Python
42
star
12

lobsterdao

Lazy airdrop based on private temporary ids
Python
41
star
13

raycast-eips

look up ethereum proposals and read them in raycast
TypeScript
38
star
14

gasprice

estimate ethereum gas price
Python
34
star
15

yearn-fees

a quest for accurate fee accounting
Vyper
30
star
16

etherscan-cache

Python
30
star
17

lido-vault

Yearn Vault wrapper for Lido St. Ether
Python
27
star
18

httpie-image

HTTPie plugin to display images in iTerm2
Python
24
star
19

cornichon

Python
23
star
20

your-eminence

distribution scripts for eminence compensation
Python
22
star
21

liqui

liqui.io api wrapper
Python
21
star
22

permit-deposit

Python
19
star
23

telegram

a very minimal telegram api wrapper
Python
18
star
24

dpack

python implementation of dpack evm packaging format
Python
18
star
25

imagehashmasks

hashmask reverse lookup
Python
17
star
26

exploit-ycredit

Python
17
star
27

strategy-uni-lp-pickle

Python
17
star
28

vmtrace

evm-trace vmtrace demo and playground
Python
17
star
29

baseline-looper

loop into YES or unwind a baseline credit account
Python
16
star
30

uniswap

uniswap analytics
Python
14
star
31

gas-costs

a quick way to calculate gas costs for a bunch of accounts
Python
13
star
32

feil-proposal

governance proposal to make fei redeemable for eth
Python
13
star
33

bitcointalk

parses bitcointalk ann topics and sends them to telegram channel
Python
12
star
34

lido-keep3r

Python
12
star
35

yearn-vault-keeper

keep yearn vaults tightly invested while maintaining the withdrawal buffer
Python
12
star
36

ape-events

ape log caching plugin
Python
12
star
37

defi-apy

Python
11
star
38

brownie-ds-proxy

Python
11
star
39

yfi-pact

our pact with the devil
Python
11
star
40

blue-pill

The Blue Pill: A spiritual guide for the past, present and future of Yearn
Python
10
star
41

vyper-cwia

vyper contract that can access immutable arguments passed as calldata
Python
10
star
42

woofy-snapshot

Solidity
10
star
43

baseline-snapshot

YES token snapshot
Jupyter Notebook
10
star
44

coinlisting

cryptocurrency exchanges new coins listing
Python
9
star
45

yearn-principles

8
star
46

parasitic-vault

Python
8
star
47

staking-deposit

simple gΓΆrli mass deposit contract
Python
8
star
48

docsend

convert docsend to pdf or png sequence
Python
7
star
49

universal-router

Python
7
star
50

snek3

msgspec definitions of ethereum execution client api
Python
6
star
51

vyper-eip6909

eip-6909: minimal multi-token interface in vyper
Vyper
6
star
52

veyfi-model

Jupyter Notebook
6
star
53

itf

Python
5
star
54

uniswap-distribution

5
star
55

maker-chief

tally makerdao governance votes
Python
5
star
56

tellor

python tools for tellor
Python
5
star
57

yearn-permit

TypeScript
5
star
58

pip-blame

finds packages that prevent upgrading a transitive dependency
Python
4
star
59

spank-uni-distribution

Python
4
star
60

curvefi

Python
4
star
61

blockfolio

blockfolio cli
Python
4
star
62

docker-as-venv

an example of how to use docker for python web development
Python
3
star
63

pink-duck

Quack quack
Python
3
star
64

sign-test

test of how github displays author/committer spoofing
3
star
65

vyper-eip1967

Vyper
2
star
66

poloniex

minimal poloniex api wrapper
Python
2
star
67

vyper-traceback-bug

looking into compat issue in ape-vyper
Vyper
1
star
68

pickle-snapshot

1
star
69

stamps

Python
1
star
70

lto-bridge

three-way lto bridge monitoring
Python
1
star
71

pytho

Akropolis IOU token
Python
1
star
72

hug_raven

Sentry integration for hug
Python
1
star
73

unswerve

liquid wrapper for vote-locked swrv
Python
1
star