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.
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.
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
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.
_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
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.
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
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.