# on manipoolator, my gmx oracle player

By [bt3gl's symposium](https://paragraph.com/@go-outside) · 2023-02-13

---

tl; dr
------

### the strategy

today i go over a **_hypothetically_ profitable strategy** that runs an **oracle manipulation attack** on [**GMX**](https://gmx.io/#/).

for my previous two bots, [**cointbot**](https://mirror.xyz/steinkirch.eth/KQ0basHaclOCDDtOhz3NgKQhHdHqaqOtU89Sr4QO5L4) and [**cowsol**](https://mirror.xyz/steinkirch.eth/s_RwnRgJvK_6fLYPyav7lFT3Zs4W4ZvYwp-AM9EbuhQ), i provided a complete and original production-level strategy and source-code. for this one, i will only supply the **_idea_**.

* * *

### the status quo

**this vulnerability is not new**, and whoever is following the space closely is probably pretty aware of it. last year, there was some conversation about adding a [**twamm mechanism**](https://github.com/go-outside-labs/mev-toolkit/blob/main/MEV_strategies/oracles/twamm.md) to the **fast price feed** (i will tell you what this feed is below).

i will leave it open to you to verify whether this strategy works (for example, by [**simulating against past data**](https://github.com/go-outside-labs/blockchain-science-rs)).

### on gray-hat hacking

**market manipulation is illegal** but openly **talking about risks is healthy and helps the space** to progress (in any case, of course i am **not** responsible for anything you do with my free code or ideas).

### all right, here goes the manipoolator…

* * *

🎶 today’s mood
---------------

[https://open.spotify.com/track/14LTdOYKE4A2MD710yWvco?si=daa8f2ac53e94110](https://open.spotify.com/track/14LTdOYKE4A2MD710yWvco?si=daa8f2ac53e94110)

* * *

🔮 gmx for the uninformed
-------------------------

**_“gmx offers zero slippage on trades via an oracle price update system (chainlink + aggregate of prices from leading volume exchanges), while amms rely on arb bots to balance prices in the pools”_**

### the exchange

[gmx](https://gmx.io/) is a decentralized spot and perpetual futures exchange built on [arbitrum](https://arbitrum.io/) and [avalanche](https://www.avax.network/) chains.

[it exploded after the ftx fall](https://stats.gmx.io/), and just this week, it seems to have become [the top defi project on fee profit](https://twitter.com/gmx_intern/status/1624272432126390278?s=20&t=JECieJ-g9DKvQiEB7y7EEQ), and [one of the top projects by fee and revenue in defillama](https://defillama.com/fees). they are winning by offering perks such as **_no slippage_**, **_low swap fees_**, and **_zero price impact trades_** (_i.e.,_ large trades are set at the [mark price](https://medium.com/mark-price/mark-price-e8f748c9b549)).

### the price feed

gmx’s swap trades are performed based on a combination of two oracles:

*   the **primary oracle** is fed by [chainlink](https://docs.chain.link/), and it takes the last three samples (picking the worst price for the user).
    
*   the **secondary oracle** is the **“fast price feed”**, which overrides the price whenever it is within 2.5% of the primary oracle price value.
    

### gml vault

gmx enables traders to open up to 50x [leverage](https://github.com/go-outside-labs/mev-toolkit/tree/main/MEV_and_trading/lending) swaps for long or short positions by borrowing from [glp](https://gmxio.gitbook.io/gmx/glp), **a multi-asset pool containing $btc, $eth, $uni, $link, and stablecoins**. the vault allows swapping of the tokens it holds.

funds are **deposited into the vault by minting glp tokens** and can be **withdrawn by burning these tokens**.

glp works as the counterparty in the protocol, as it accrues values when traders loses, and devalues when traders win.

glp is also emerging as a form of collateral, with lending protocols integrating this liquidity provider token into their product offerings (_e.g.,_ [rage, unami, sentiment](https://github.com/go-outside-labs/mev-toolkit/blob/main/MEV_by_chains/MEV_on_Arbitrum/gmx/glp_vaults.md)).

### $gmx

gmx’s native token, [$gmx](https://www.coingecko.com/en/coins/gmx), functions as a governance, utility, and value-accrual token. **glp accrues 70% of all trading fees, while stakers of gmx earn 30%**.

a **floor price fund** helps ensure liquidity in the glp pool, plus a reliable stream of $eth rewards for $gmx stakers.

### protocol fees

the protocol's revenues come from swap fees, trading fees, execution fees, liquidation fees, and borrow fees.

fees are set [in the vault’s contract](https://github.com/gmx-io/gmx-contracts/blob/master/contracts/core/Vault.sol):

![](https://storage.googleapis.com/papyrus_images/cfcab3d6ed9cda73f24bd69f5c88b96778816db4d9efc928c025285b49b50e0d.png)

### liquidations

gmx’s [keepers](https://gmxio.gitbook.io/gmx/trading#execution-fee) can liquidate a position if the position reduces to a point at which the collateral minus losses minus borrow fee becomes less than 1% of position's size.

### protocol contracts

the gmx protocol is composed of the [following parts](https://gmxio.gitbook.io/gmx/contracts):

1.  **the vault contracts** handle trading functions and deposits of usd to glp (named usdg). it includes **the price feeds contracts,** which handle the two price feed oracles.
    
2.  **the router contract** implements convenient functions on top of the vault. it’s also used to help mitigate frontrunning attacks through a two-step transaction process.
    
3.  **the gmx governance contracts and the glp liquidity provider contracts,** are regular erc20 tokens utilized by the protocol.
    

for the strategy, we will be looking at the first.

* * *

🔮 the vault and price contracts
--------------------------------

the main [vault](https://github.com/gmx-io/gmx-contracts/blob/7ce789b40086eb2eda71d72ebbe6468f8c1c8f3b/contracts/core/Vault.sol) contract is `Vault.sol` (on [arbitrum](https://arbiscan.io/address/0x489ee077994B6658eAfA855C308275EAd8097C4A#code) and [avalanche](https://snowtrace.io/address/0x9ab2De34A33fB459b538c43f251eB825645e8595#code)) and is used to handle gmx’s trading functions.

here is a list of the external and public methods:

*   `swap(address _tokenIn, address _tokenOut, address _receiver)`
    
*   `getMaxPrice(_token)`
    
*   `getMinPrice(_token)`
    
*   `buyUSDG(address _token, address _receiver)`
    
*   `sellUSDG(address _token, address _receiver)`
    
*   `directPoolDeposit(address _token)`
    
*   `increasePosition(address _account, address _collateralToken, address _indexToken, uint256 _sizeDelta, bool _isLong)`
    
*   `decreasePosition(address _account, address _collateralToken, address _indexToken, uint256 _collateralDelta, uint256 _sizeDelta, bool _isLong, address _receiver)`
    
*   `liquidatePosition(address _account, address _collateralToken, address _indexToken, bool _isLong, address _feeReceiver)`
    
*   `validateLiquidation( _account, _collateralToken, _indexToken, _isLong, _raise)`
    
*   `getRedemptionAmount(_token, _usdgAmount)`
    
*   `getRedemptionCollateral(_token)`
    
*   `getRedemptionCollateralUsd(_token)`
    
*   `tokenToUsdMin(_token, _tokenAmount)`
    
*   `usdToTokenMax(_token, _usdAmount)`
    
*   `usdToTokenMin(_token, _usdAmount)`
    
*   `usdToToken(_token, _usdAmount, _price)`
    
*   `getPosition(_account, _collateralToken, _indexToken, _isLong)`
    
*   `getPositionLeverage(_account, _collateralToken, _indexToken, _isLong)`
    
*   `getUtilisation(_token)`
    
*   `getNextAveragePrice(_indexToken, _size, _averagePrice, _isLong, _nextPrice, _sizeDelta, _lastIncreasedTime)`
    
*   `getNextGlobalShortAveragePrice(_indexToken, _nextPrice, _sizeDelta)`
    
*   `getGlobalShortDelta(_token)`
    
*   `getPositionDelta(_account, _collateralToken, _indexToken, _isLong)`
    
*   `getDelta(_indexToken, _size, _averagePrice, _isLong, _lastIncreasedTime)`
    
*   `getEntryFundingRate(_collateralToken, _indexToken, _isLong)`
    
*   `getFundingFee(_account, _collateralToken, _indexToken, _isLong, _size, _entryFundingRate)`
    
*   `getPositionFee(_account, _collateralToken, _indexToken, _isLong, _sizeDelta)`
    
*   `getTargetUsdgAmount(_token)`
    

**the one we are interested in is** `swap()`**.**

❓how does swapping asset work

❓how does it know `_tokenIn` and `_tokenOut` prices, so it can send the correct amount to `_receiver`

![](https://storage.googleapis.com/papyrus_images/4d194292cede7bf5d09de898f844a7da538c3b227fb8c8172204a1ddc774b6b4.png)

assets are swapped by the prices given by `priceIn` calling `getMinPrice(_tokenin)` and `priceOut` calling `getMaxPrice(_tokenOut)`:

![](https://storage.googleapis.com/papyrus_images/0f8c8a6fc4a38232b26b11c48b41da73c9c79796f359f91e0ff8f4ef49e15167.png)

nice, we are hitting the price methods.

remember that the price feed contracts implement a primary price mechanism that samples the three last chainlink oracle values and picks the worst for the trade. from [gmx’s docs](https://gmx-io.notion.site/gmx-io/GMX-Technical-Overview-47fc5ed832e243afb9e97e8a4a036353):

> _The vault uses the price from the keeper if it is within a configured percentage of the corresponding Chainlink price. If the price exceeds this threshold, then a spread would be created between the bounded price and the Chainlink price, this threshold is based on the historical max deviation of the Chainlink price from the median price of reference exchanges. For example, if the max deviation is 2.5% and the price of the token on Chainlink is $100, if the keeper price is $103, then the pricing on the vault would be $100 to $103._

so, this primary price mechanism is overridden when the secondary (fast feed) is enabled and the price is within a 2.5% range. this also gives a different price depending on the direction (it has lower chances of being > than an amm).

let’s check how the snippet above calls `getPrice(),` which is implemented by [the vault contract for the price feed](https://github.com/gmx-io/gmx-contracts/blob/master/contracts/core/VaultPriceFeed.sol), `VaultPriceFeed.sol`:

![](https://storage.googleapis.com/papyrus_images/c77784c9113a4ae4cb4f2bf759b2cd9e9bf794c077a37e934cdf0127fa35016d.png)

we see two options to get the asset’s price.

when `useV2Pricing()` is set to `false` ([like here](https://github.com/gmx-io/gmx-contracts/blob/master/contracts/core/VaultPriceFeed.sol#L31)), `getPriceV2()` is fired:

![](https://storage.googleapis.com/papyrus_images/3d354d0eb867bf050bb7f25f2020cee5adae56ed108c2cedc8f779986b6ab62e.png)

here is where the fun starts.

while the first price mechanism, `getPrimaryPrice()` simply calls [chainlink](https://github.com/gmx-io/gmx-contracts/blob/master/contracts/core/VaultPriceFeed.sol#L287):

![](https://storage.googleapis.com/papyrus_images/0879d0af9692f86a01f249b42e0248989ddbcb1f63e5be3e5c362353c68fcf4c.png)

[**isAmmEnabled**](https://github.com/gmx-io/gmx-contracts/blob/master/contracts/core/VaultPriceFeed.sol#L29) is usually set to `true` (it’s `false` in liquidation cases), so gmx’s prices are pretty much coming from `getAmmPriceV2()` and then `getAmmPrice()`:

![](https://storage.googleapis.com/papyrus_images/a11ba4fb02b325fcaa88edb93e283a347119a85629f74ac941c5fc9075390a50.png)

yes, this is our good ol’ **x\*y=k price amm**,

![](https://storage.googleapis.com/papyrus_images/50d6e251b403f019a642cd0cda09bd9b5ede960f753df966b85ffb876221cd49.png)

**this** [**amm**](https://github.com/gmx-io/gmx-contracts/tree/master/contracts/amm) **could be vulnerable to manipulating markets. in other words, updates of gmx’s secondary oracle could, in theory, be sandwiched.**

because of the way `swap()` is called and the fact that gmx does not implement slippage, many arbs between gmx and secondary markets could be done until they are at the same price (or until a particular delta market is obtained).

> 💡 _In the context of trading, delta (Δ) is a risk metric that estimates the change in the price of a derivative, given a $1 change in its underlying security. The delta also tells the hedging ratio to become delta neutral (_[_more_](https://www.investopedia.com/terms/d/delta.asp)_)._

in theory, a secondary market with enough liquidity could allow our **manipoolator** to drain all the liquidity of gmx (well, proportional to the slippage of this secondary market):

`gain = gmx_price / (secondary_market_price * gmx_liquidity)`

now think about that 50x leverage. it could mean 150% gain on the initial position.

* * *

🔮 manipoolator as a sandwichor in avalanche
--------------------------------------------

arbitrum runs on [a centralized and ill-documented sequencers](https://developer.arbitrum.io/inside-arbitrum-nitro/), so it does not have a public mempool. to get the right block timing, our **manipoolator** would have to run a strategy based on trial & error (while _praying_ for [fss](https://github.com/go-outside-labs/mev-toolkit/tree/main/MEV_by_chains/MEV_on_Arbitrum/fair_ordering_sequencing) not to be implemented soon).

but a sandwich attack could be possible on avalanche ([here is a cool mev story on avalanche c-chain](https://www.ddmckinnon.com/2022/11/27/all-is-fair-in-arb-and-mev-on-avalanche-c-chain/)).

**manipoolator** would take advantage of pricing updates for the assets in the glp vault. it could search the mempool for large updates through txs containing `SetPrices()` or when the fast price feed is updated (_e.g.,_ when keepers send txs to execute it).

![ ](https://storage.googleapis.com/papyrus_images/55e1b8b927119314a602dbbc587ab7b18d5375120310b17753017e72e7708907.png)

(btw, [PriceFeed.sol](https://github.com/gmx-io/gmx-contracts/blob/7ce789b40086eb2eda71d72ebbe6468f8c1c8f3b/contracts/oracle/PriceFeed.sol) is the contract that accepts submissions from the price feed keeper, using the median price of binance, bitfinex, and coinbase)

at that point, **manipoolator** could

1.  frontrun the tx to execute the order with a [flashloan](https://github.com/go-outside-labs/mev-toolkit/tree/main/MEV_strategies/flashloans), buying an asset before the prices increases,
    
2.  close its position right after the price change, and
    
3.  profit the delta (after paying the opening-position-margin-funding-rate-closing-position-margin fees).
    

something like this:

![sandwich on gmx's fast price feed.](https://storage.googleapis.com/papyrus_images/36d9e1bac973515ed7878715312aae461768892ea59e2735d24b4b62a774a6af.png)

sandwich on gmx's fast price feed.

* * *

🔮 manipulating manipoolator
----------------------------

🤔 we are not even talking about multiple pools or cross-chain yet… infinite possibilities?

🤔 would increasing trading fees lower the chances of an oracle manipulation attack (at least during large volatility)? but then, would gmx stop “winning”?

🤔 would trying to prevent blocks with more than two `SetPrices()` txs help avoid sandwich attacks? yeah, but how?

🤔 or, in a different direction, _what if_ the prices of the secondary markets could be manipulated to make them equal to the primary oracle, undermining the second oracle? what would that mean?

🤔 would **manipoolator** try to remain stealthy, or would it go _all in_ during a volatile event?

* * *

🔮 irl
------

there have been some reports of exploitation of the gmx’s oracles last year, with _somehow_ similar ideas described in this post.

one of the shenanigans was that while the gmx team runs keepers to update prices by making calls to `SetPriceWithBit()`, mev operators could observe these price updates in the mempool before they are on-chain.

[https://twitter.com/joshua\_j\_lim/status/1571554171395923968?s=20&t=bLhokqUbLtWaOps04GJgwA](https://twitter.com/joshua_j_lim/status/1571554171395923968?s=20&t=bLhokqUbLtWaOps04GJgwA)

[https://twitter.com/ChainsightLabs/status/1580208615654584321?s=20&t=0fbE1f5XfD8b-l0zzRmQow](https://twitter.com/ChainsightLabs/status/1580208615654584321?s=20&t=0fbE1f5XfD8b-l0zzRmQow)

[https://twitter.com/wilburforce\_/status/1571891338097860608](https://twitter.com/wilburforce_/status/1571891338097860608)

on another note, during a major update regarding synths, gmx added a new oracle called **xget** with frontrunning protection:

[https://twitter.com/Kalcrypto1/status/1576977723016085505?s=20&t=NLpve9weGE\_Dnoxit-NQ0A](https://twitter.com/Kalcrypto1/status/1576977723016085505?s=20&t=NLpve9weGE_Dnoxit-NQ0A)

* * *

**◻️ motherofbots.eth**
-----------------------

---

*Originally published on [bt3gl's symposium](https://paragraph.com/@go-outside/on-manipoolator-my-gmx-oracle-player)*
