Opyn Hacks: Root Cause Analysis

PeckShield
3 min readAug 5, 2020

--

Started at 09:25:54 AM +UTC, Aug-4–2020, the decentralized insurance product, Opyn, was attacked by exploiting its flawed handling of ETH reception in its Opyn ETH Put smart contract. Opyn published a medium post about this incident. Here we will elaborate the technical details in this blog post.

Summary

This hack was done by calling exercise() with more than two vaults with ETH as the underlying assets. Since the implementation treats the same batch of ETH received as multiple batches of ETH receptions, the hacker re-uses that batch of ETH to retrieve the collateral USDC and make profits.

Details

Opyn allows anyone to exercise a vault with adequate underlying assets and oTokens. By burning the oTokens and taking in the underlying assets, the OptionContracts pays out collateral assets to the caller of exercise().

Figure 1: exercise() Loops a List of Given Vaults

As shown in the above code snippet, the exercise() function loops through the given list of vaults, vaultsToExerciseFrom[], until the oTokensToExercise is consumed. Note that the _exercise() internal function is invoked to do the real exercising thing in each iteration of the loop.

Figure 2: Re-using the ETH Sent into the Contract to Retrieve Collateral

Inside the _exercise() function, the Opyn ETH Put contract needs to take in the underlying assets and burn oTokens before paying collateral assets out. While handing ERC20 assets, similar to most DeFi projects, the transferFrom() is called (line 1882) to take the assets from the msg.sender to address(this), which is a pretty common practice. When the underlying assets is ETH, the handling is totally different. In Solidity, the msg.value means the amount of ETH carried by current transaction which would be collected by the smart contract with a payable interface (i.e., exercise() in this case). By checking msg.value == amtUnderlyingToPay (line 1879), the smart contract ensures that it has received amtUnderlyingToPay of ETH. However, as we mentioned earlier, the _exercise() function could be invoked multiple times in the loop. This leads to the same msg.value amount of ETH would be re-used in the second or further _exercise() calls in the same transaction. But, only the first batch of ETH is received.

Figure 3: Exploit Transaction

In Figure 3, we illustrated the exploit transaction with Bloxy. The bad actor intentionally exercise() two vaults with 150 oETH tokens and 75 ETH. Those two 24,750 USDC transfers show how the 75 ETH collateral assets was re-used.

Mitigation

While dealing with ETH reception, we typically use a local variable msgValue to keep the amount of msg.value in Solidity. This allows us to calculate and book-keep how much ETH had been taken. In addition, address(this).balance could be used to check if the smart contract does have enough ETH as indicated by msg.value.

About Us

PeckShield Inc. is an industry leading blockchain security company with the goal of elevating the security, privacy, and usability of current blockchain ecosystem. For any business or media inquiries (including the need for smart contract auditing), please contact us at telegram, twitter, or email.

--

--