Opyn Hacks: Root Cause Analysis
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()
.
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.
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.
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.