On Apr 02, 2022, 11:04:09 AM UTC (block 14506359), the attacker borrowed assets from Inverse Finance using a collateral asset that had less actual value than the borrowed assets. The price of the collateral asset on Inverse Finance becoming more expensive at the time, making $INV collateral with more borrowable value than it should be. We will go over the technical details of this attack step by step in this article.
Related Address
- Inverse Finance Exploiter #1: https://etherscan.io/address/0x117c0391b3483e32aa665b5ecb2cc539669ea7e9
- Inverse Finance Exploiter #2: https://etherscan.io/address/0x8b4c1083cd6aef062298e1fa900df9832c8351b3
- Exploiter’s Contract: https://etherscan.io/address/0xea0c959bbb7476ddd6cd4204bdee82b790aa1562
- Keep3rV2Oracle: https://etherscan.io/address/0x39b1df026010b5aea781f90542ee19e900f2db15
Attack Steps
There are 2 main transactions used for the successful attack as follows:
1. Price manipulation, Update the price on Oracle, and Swap tokens at transaction: https://etherscan.io/tx/0x20a6dcff06a791a7f8be9f423053ce8caee3f9eecc31df32445fc98d4ccd8365
1.1 Verified that the price feed can be updated.
1.2 Exploiter’s Contract allowed using $INV as collateral on the lending contract.
1.3 Manipulated price by swapping 300 $WETH to 374.385477084842174221 $INV on the SushiSwap’s $WETH-$INV pair.
1.4 Updated the price feed. The price feed of every pair was updated by calling the workForFree()
function.
The keep3rV2Oracle
contract that refer to the SushiSwap’s $INV-$WETH pair (address 0x39b1df026010b5aea781f90542ee19e900f2db15
) is one of updated feed.
The price0CumulativeLast
value after the update is 0x00000000000000000000000000000062d32f53f2f7afe532c0372fd0cacdbd4b
, which the Oracle will calculate this value with e10
and Q112
. Then the value from the calculation, 6476591327926140254201750
, will be stored in the Oracle’s observation.
It goes with the same as the price1CumulativeLast
value, 0x000000000000000000000000000012d5e533107a3e9659278cb49bb1e692524d
, which will be calculated into 316007731064365302759283159
and being stored into the Oracle’s observation.
At this update, Oracle has stored the manipulated cumulative price into the observation with timestamp 1648897434
at index number 114
of the observations.
1.5 Swapped 200 $WETH to 690,307.061277 $USDC on SushiSwap.
1.6 Exchanged 690,307.061277 $USDC to 690,203.010884231600886834 $DOLA on Curve’s $DOLA + 3Crv pool.
1.7 Swapped 690,203.010884231600886834 $DOLA to 1,372.052401667461914227 $INV on Sushiswap’s $INV-$DOLA pair.
2. Lend and Borrow at: https://etherscan.io/tx/0x600373f67521324c8068cfd025f121a0843d57ec813411661b07edc5ff781842
Attacker lent 1,746.437878752304088448 $INV
Returned price from the getUnderlyingPrice()
function of $INV feed is 20926.791034009538953802 $USD.
The collateral factor at the lent/borrowed time is 60% (0.6 e18) of the asset value.
By the 1,746.437878752304088448 $INV lent, total value in $USD that the attacker can borrow is $21,928,404.32551701 (60% of 36,547,340.54252835)
The price of other related assets in the same transaction:
- $WBTC is $46650.31
- $ETH is $3488.82
- $YFI is $23461.259
Attacker borrowed the following assets:
- 3,999,669.029654761043260989 $DOLA
- 1,588.263719446159096974 $ETH
- 94.03071805 $WBTC
- 39.368440899328442 $YFI
Finally, the attacker transferred all borrowed assets to Inverse Finance Exploiter #2’s wallet.
Root Cause Analysis
The core oracle that contains the price calculation logic of Inverse Protocol is the Keep3rV2Oracle
contract.
The following code is the current()
function of the Keep3rV2Oracle
contract.
Keep3rV2Oracle.sol
The current()
function returns the price of the asset. It is mainly used for calculating the getUnderlyingPrice()
function to determine the asset value.
The current()
function takes the last stored cumulative price from the observation and the current cumulative price to calculate the price. The function has a flaw that allows the calculation with a small timeElapsed
value.
The problem will arise when the current()
function is called 1 block after the block that the price feed has been updated, which is the case that the attacker has leveraged in this attack.
The attacker manipulated the $INV price in block number 14506358
and the Oracle stored the manipulated cumulative price in the observations. At block number 14506359
, the current()
function used the recently updated cumulative price from the observations and the current block’s cumulative price.
To guarantee the latest cumulative price being set, the attacker has to manually force the price feed (SushiSwap) to update the cumulative price. So, the attacker called the sync()
function first at the transaction 0x600373f67521324c8068cfd025f121a0843d57ec813411661b07edc5ff781842.
Here’s the catch, the current block’s cumulative price was calculated from last price (the attacker force update the price to ensure this value) of the previous block multiplying with the current block time (15 seconds in this case). When the current()
function is calculating _computeAmountOut()
, the current block’s cumulative price is the latest cumulative price plus with the multiplying of the price and the passing time. When putting every parameter into the calculation of _computeAmountOut()
function
From now on, we will simplify the equation by ignoring the scaling constant *e10/Q122
from price0Cumulative
and _observation.price0Cumulative
.
The current block’s price0Cumulative
is retrieved from IUniswapV2Pair(pair).price0CumulativeLast()
.
The _observation.price0Cumulative
is replaced with a placeholder variable {lastBlockCumulativePrice}
to refer to the cumulative price from block number 14506358
onward.
The variable IUniswapV2Pair(pair).price0CumulativeLast()
is replaced with {lastBlockCumulativePrice} + (lastBlockLatestPrice*blockTime)
. This is how the price cumulative of block number 14506359
being calculated.
We remove the parentheses from {lastBlockCumulativePrice} + (lastBlockLatestPrice*blockTime)
to make the equation be more clearer.
In the left parentheses, {lastBlockCumulativePrice}
can be canceled each other out.
Then we remove the lastBlockLatestPrice*blockTime
from the parentheses.
The blockTime
from price0Cumulative
is the length time between block used in the calculation of cumulative price, which can be observed from the getReserve()
function. The timestamp of block number 14506358
is 1648897434
and the timestamp number 14506359
is 1648897449
. So, the value of blockTime
is 15
, which is also the same number of the parameter elapsed
that we had considered having a flaw earlier. The value of blockTime
and elapsed
is the same, which will be canceled out in the calculation in the _computeAmountOut()
function.
Long story short, this oracle will return the price from the last block last reserve price directly (with a condition, of course). The price from the Oracle in this block (14506359) will be the price that being manipulated from the last block (14506358).
Conclusion
In summary, the attacker spent around $1,737,310 (500 $ETH) to manipulate the price of $INV on Inverse Finance and exchange all assets for $INV. All exchanged $INV is used as collateral to borrow $DOLA, $ETH, $WBTC, and $YFI. The total value of borrowed assets is around $14,843,389.
About Inspex
Inspex is formed by a team of cybersecurity experts highly experienced in various fields of cybersecurity. We provide blockchain and smart contract professional services at the highest quality to enhance the security of our clients and the overall blockchain ecosystem.
For any business inquiries, please contact us via Twitter, Telegram, contact@inspex.co