/evil-jar

evil jar attack technical post-mortem

Primary LanguagePython

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