This vulnerability was submitted directly to Perpetual Protocol with a payout of $10K
Any system which offers $1 of rebates for $1 of trading fees can be exploited via wash trading. Wash trading involves creating 2 accounts and then executing trades between them, one as a maker/LP, second as a taker. Part 1 shows how Perpetual Protocol fits this description due to their liquidity mining rewards formula being based on transaction fees collected. The AMM fee system which gives most/all trading fees to makers means that wash trading is profitable even in market conditions where liquidity mining rewards are very low. Part 2 describes 3 ways to execute this wash trading exploit.
We propose a solution at the end of the report – a slight change to the liquidity mining rewards which still rewards liquidity providers based on the value the provide to traders while preventing all wash trading exploits.
Part 1: Why Perpetual Protocol Liquidity Mining Rewards Can be Wash Traded
Similar Case study – IDEX Wash trading
To explain the Perpetual Protocol’s exploit, it helps to look at a similar situation. IDEX is a decentralized spot exchange which has been heavily wash traded since they launched their trading rewards program. A limit order book on top of a Uniswap v2 protocol.
But Doesn’t Perpetual Protocol have a Liquidity Mining Program, not a Trading Rewards Program?
Your liquidity mining rewards each week = The tx fees you earned across markets this week / The total tx fees earned on the protocol this week * The liquidity mining rewards this week
Rewards are proportional to trading volume facilitated as a maker/LP. As a wash trader you own both the maker and taker account. Therefore, the “Liquidity Mining Rewards” act exactly as a trading rewards program, except that 100% of the rewards goes to the maker.
There is a bonus rebate which is that the maker in a Uniswap v3 pool gets paid for their market making order (80% of trading fees) while the maker in an order book exchange like IDEX/FTX has to pay fees to the protocol.
Table comparing IDEX and Perpetual Protocol Wash Trading:
| Protocol Features | IDEX | Perpetual Protocol | Wash Trading Implications |
| Liquidity | Uniswap v2 liquidity with a limit order book. | Uniswap v3 liquidity. | IDEX wash trading is best performed with limit orders. You can use 1 tick liquidity to imitate a limit order to wash trade, or provide a very concentrated liquidity to capture the majority of trading volume. |
| Exchange type | Spot Only | Perpetual Swaps only. | Not a big difference for wash trading. |
| Referral Program | 80% of trading fees to referrer | 20% trading fees to referrer | Referral rewards act as an additional reward for wash trading. You simply make two wallets and have one refer the other to collect the referral rewards yourself. |
| Rewards Program | IDEX token to traders proportional to trading volume. Equal weight to maker and taker orders. | PERP and OP token for makers. | When you’re both the maker and taker of a trade, it doesn’t really matter which account the trading rewards go to. |
| Trading fees. | 0.1% trading fee for makers, 0.25% trading fee for takers. | 80% of trading fees go to Makers/LP’s. 20% goes to LP | This means that Perpetual protocol is profitable to wash trade even with very low rewards. This is because you can get almost 100% of the trading fees paid rebated (80% back into the maker account, 20% through referral rewards). IDEX, in contrast, charges makers a fee rather than paying them for trading volume, meaning that referral+token incentives are the only source of trading rebates. |
Above: Wash trading on the ETH pool during April. 10.9K USD swaps between ETH and USD to farm the trading fee rewards.
IDEX is still being wash traded today but mainly on stablecoin pools. User spamming $15000 wash trades on the DAI/USDC pool.
These wash trades are made by making a large limit order in account 1 then immediately taking that order on account 2. We can do a similar thing in PERP, concentrating the liquidity in a single tick as a synthetic limit order:
1. Put a large amt of $ in the tick above the price of the PERP
2. Put a order slightly larger than the size of the liquidity
3. Close that position.
4. Remove liquidity.
IDEX isn’t unique for being heavily wash-traded. A few other examples where the majority of trading volume was wash traded: dYdX, ImmutableX, LooksRare, DeversiFi
Breakdown of Fee rebates for Perpetual Protocol
1. 20% Referral fee rebate
2. 80% of fees going to makers
3. The “liquidity mining” rewards which are linearly proportional to fees collected by makers.
Imagine a scenario where someone controls 100% of the liquidity and executes an order against their own liquidity pool. They pay $1 in in trading account. Get 0.8 in their liquidity account. They get 0.2 in their referral rewards account.
Trader pays $1 of fees.
$0.8 goes to the LP
$0.2 goes to perp stakers.
Trader gets $0.2 rebate
LP goes 0.8 rebate
They get 100% of their trading fees back. This is without any token incentives. Realistically we can never capture 100% of the liquidity of a tick, but we can capture a large part of it and get >100% trading fee rebate when including token incentives.
Key takeaway: in vAMMs almost all the trading fees are given to the maker/LP so even very small trading rewards allow for profitable wash trading.
Thresholds for profitability:
Wash trading is profitable as long as trading fee rebate is >100%. At current market conditions we can get $2-3 of rebates (LM rewards+LM fee+Referral reward) for every $1 on most trading pools if we own 90% of the liquidity. Wash trading for these pools would still be profitable even if the trading rewards were 10 times lower.
With 90% of liquidity pool we need rebate to be 0.11x base APR
w/ 75% of liquidity pool we need 0.33x base APR
These are representative of the ratios across every pool except ETH
We can calculate the $ rewards per $ of trading volume by dividing the rewards APR to base APR on the Perpetual Protocol UI. These tables only include assets beginning with A, but is representative of other listed assets.
TOTAL REWARDS FOR LIQUIDITY POOL PER UNIT OF TRADING ACTIVITY
REWARDS PER $1 OF TAKER TRADING FEES FOR LIQUIDITY PROVIDERS THAT OWN A LARGE PERCENTAGE OF THE LIQUIDITY POOL
If this exists, why hasn’t it been exploited in an obvious way yet?
- It is far less obvious that wash trading works in Perpetual Protocol because it requires reframing the liquidity mining formula as a trading rewards formula for makers.
- IDEX’s limit orders make wash trading trivially simple. Although 1-tick liquidity in an AMM can simulate a limit order, the ticks are not granular enough to wash trade exactly the same way as in a Central Limit Order Book. Part 2 will explain 3 ways to wash trade on Perpetual Protocol
Part 2 – Three Approaches to wash trading
We designed multiple wash trading techniques in case Perpetual Protocol has countermeasures to certain forms of trading activity. Additionally these approaches have different requirements and downsides. The first approach is our ideal one – it suffers zero impermanent loss and captures close to 100% of the trading rewards as profits as it can capture almost 100% of the liquidity for a wash trade. The last approach should be performed with 75-90% of the liquidity, has potential exposure to toxic flow, but works even when external participants interrupt our trades.
Account balance required (est. 200K): With $200k of account balance we can provide $2M of liquidity. We are using extreme concentration around the price point – more than a genuine LP, so the $2M of liquidity enough to dominate the liquidity of most pools.
Meta-Approach
You will repeat these approaches on every liquidity pool. One approach is spreading the wash trades into different periods out, trying to game a smaller share of the trading rewards rather than going for the maximum exploitable amount, and switching between different wash trading techniques on the same pool to avoid being noticed. This allows you to exploit for multiple periods.
Another approach is to focus the trades onto the last day of the liquidity mining period so you immediately receive the lions share of the rewards of one period. Many protocols don’t notice even the most obvious wash trading activity and are exploited for months.
First approach (1-tick liquidity wash trade):
Requirements: Be able to execute 4 transactions in a block
1. Put a large amt of $ in the tick above the price of the PERP
2. Put a order slightly larger than the size of the liquidity
3. Close that position.
4. Remove liquidity.
Capture 99% of the liquidity pool.
The first approach attempts to port the IDEX wash trade directly over.
This uses the 1 tick of liquidity as a synthetic limit order. As this liquidity is magnitudes more concentrated due to being within a single tick, we can get almost 100% of the trading fees we paid back via our liquidity position and our 20% trading fee rebate.
The second 2 approaches we want to capture 75%-90% of the liquidity pool. That is because we are limited by our desired exposure to impermanent loss. We want to do this with a 10x levered liquidity position at price range +-3%.
Second approach (wash trading within concentrated liquidity):
Requirements: Be able to execute 2 transactions (long a Perp and immediately close that long) in a block.
- Provide liquidity in a +-3% range.
- Execute these 2 transactions without interruption:
- Long the perp
- Close the long
- Repeat this transaction pair until we have reached our desired trading volume.
- Remove liquidity.
Costs: We will potentially incur impermanent loss due to not being able to close out liquidity instantly. Therefore we want to spam the wash trade pairs as quickly as possible. In the 1st case since our liquidity is so concentrated we can easily capture 100% of the fees and rebate. In the 2nd case we would want to limit liquidity somewhat to minimize the impermanent loss, even though we are providing liquidity for a short duration.
Factors to consider: The time we need to provide liquidity to complete wash trading.
Third approach (Wash trading within an arbitrage-resistant zone):
Requirements: Does not require doing any set of transactions in a single block
The second approach is designed for if we cannot execute the first approach, or we cannot execute those 4 transactions in a row. This doesn’t require executing any transactions sequentially without interruption. Even if there are transactions that go between the trades, there is no EV loss to the trading account. There can be loss due to changes to the underlying which will be captured under impermanent loss. This one works in the presence of external arbitrageurs. The downside is its not instant which means that we incur some impermanent loss risk.
The downside to approach 2 is losses due to an arbitrage trade occurring between opening and closing our position, which can make us lose money. We can avoid this kind of loss by restricting the price impact of our trades to stay within a zone where we do not lose to arbitrage transactions.
1. Provide liquidity on a PERP, with +-3%.
2. Wash trade within that liquidity, but with each trade only pushing the perp price within +-0.1% of the FTX price.
For Step 2 we can adjust the code from the open source arbitrage bot provided by Perpetual Protocol.
How the wash trading bot will work
We will create a wash trading bot by altering the open source Perp Arbitraguer. We will keep the spread the same, but adjust/remove the “spread” part of the code.
SLIPPAGE
MAX_SLIPPAGE_RATIO: Big(0.001) // the trade will not incur more than 0.1% slippage on Perpetual Protocol.
We remove the spread requirements for the Perp arbitraguer and repalce it with the algorithm:
When PERP_PRICE<FTX_price ,LONG until perp price reaches 1.001*FTX_PRICE
When PERP_PRICE<FTX_price ,SHORT until perp price reaches 0.999*FTX_PRICE
SPREAD
PERPFI_SHORT_ENTRY_TRIGGER: Big(0.5).div(100)
PERPFI_LONG_ENTRY_TRIGGER: Big(-0.5).div(100)
// spread ratio: ( PERP — FTX ) / FTX of the same perpetual contract price. For example, with the default values, the bot will short the contract on Perpetual Protocol when the spread ratio is greater than 0.5. At the same time, it will open a long position at FTX. When the spread ratio goes below -0.5, a long position will be opened to cancel the active position on Perpetual Protocol, and the position on FTX will be closed.
Our adjustments to make a wash trading bot:
For our wash trading bot we simply remove the spread, and make the bot trade in a way that minimizes exposure to toxic flow no matter the perp price.
//There does not need to be any spread to trade. If the price <FTX_Price, bot will long. If price>FTX_Price, bot shorts. There is no hedging on FTX.
When PERP_PRICE < FTX_price ,LONG until perp price reaches 1.001*FTX_PRICE
When PERP_PRICE < FTX_price ,SHORT until perp price reaches 0.999*FTX_PRICE
Arb bot vs wash trading bot
The arb bot is usually not trading. There is a “dead zone” between the thresholds of arbitrage. Arbitrageurs keep the price between these two ranges.
The wash trading bot is ALWAYS trading. It is programmed to trade no matter the price of the PERP relative to the oracle.
There is a deadzone between $99.7 and $100.3 where the bot does not trade. In an environment with multiple arbitrageurs, the price will remain within this zone, causing low trading volume.
The wash trading bot has no deadzone – it will trade no matter what the price of the PERP is relative to the oracle. This allows it to execute many trades per minute.
The wash trading bot has three key properties:
- Executes a trade no matter what the price point of the Perp which ensures high trading volume in a short amount of time.
- Does not make -EV trades. All its trades either push the Perpetual price towards Oracle price, or stay within the arbitrage resistant zone.
- Pre-emnptively stops toxic flow on the maker account by keeping the price of the perp on Perpetual the same as the Oracle Price (+-0.1%)
How does this preemptively stop toxic flow?
There are 2 types of trades that can be executed on our liquidity pool:
Non-toxic/random flow: These are +EV to us as we gain from the price impact trading fees from liquidity+the rewards these qualify us for.
Arbitrage trades/toxic flow: These are +EV to us as long as the price stays within the +-0.1% threshold. In the example without arbitrage trade, we do an “arbitrage” transaction which actually loses money. The “arbitrageur” actually loses money because they are paying an 0.1% transaction fee. The LP gains this.
Lets say there is a 0.1% depeg. The bot arbs it back to the original price. Firstly, this trade loses money for the trader as they have to pay an 0.1% trading fee. The arbitrageur pays a 0.1% trading fee and captures 0.05% of EV. We established that for every $1 of trades that goes through this liquidity, our liquidity pool which captures 75%-90% captures >$1. Therefore we gain more than $1 for every $1 lost in EV through the external arbitrage.
Another way of thinking of this is that we wanted to do this arbitrage anyway on our wash trading bot. The arbitrage trade is executed by an external party.
This is why bot that is inferior to others latency and other optimizations doesn’t matter. We don’t need to compete with other bots using speed because it is just as good if another bot beats us to an -EV arbitrage which increases the trading fees for our maker account.
The maximum that this approach will lose is the permanent loss of a short period of liquidity provision. However most of this is loss is captured/pre-emptively stopped by our wash trading bot. The bot constantly updates the price on Perpetual to the Oracle Price.
This approach actually suffers far less impermanent loss than a normal LP pool. The wash trading bot has a positive externality of updating the perp price to constantly match the FTX_Price which preemptively captures the toxic flow which a normal LP will suffer.
Another way to understand this is that our wash trading bot has a huge unfair advantage in arbitrage. While a normal arbitrage bot would need to wait for the difference between the Oracle Price and Perp Price to be large enough to be profitable, overcome the 0.1% trading fee and cover hedging costs.
Our wash trading bot prefers to pay fees rather than avoids it so does not need a spread to trade. Our trades are also automatically hedged because we own most of the liquidity pool which automatically takes the opposite side to our trades.
Additionally, we are only providing liquidity for the short wash trading duration, while a real LP is providing liquidity for 24 hours * 7 days to qualify for their share.
Can we trade significant size while maintaining this price impact? How many trades does this take?
We estimate anywhere from 4 minutes to 30 minutes
Since we are providing liquidity tightly around the price of the perp, we increase the trading volume required to push the price. For example, if the Aave pool currently requires $1800 to move the price 0.1%, if we add the liquidity depth to capture 90% of the liquidity pool we increase the trade size to have the same price impact to $18000.
I would estimate that currently for a pool like Aave at time of writing if you wanted to capture half the trading rewards for a period:
75%-90% of liquidity= 7200-19000 per trade.
If you own 75% of the liquidity pool you need approximately 650 trades
If you own 90% of the liquidity pool you need approximately 250 trades
The bot is CONSTANTLY under command to trade. At 1 trade/sec it will be finished from 4-10minutes. At 1 trade per 3 sec, it will take 12-30 min. The time taken is important as this approach is exposed to impermanent loss for the duration of liquidity.
Is it risky to provide this depth of liquidity?
There is loss which is why we prefer the earlier approaches. We minimize impermanent loss
The price of the perp/underlying asset changes over time. Since these transactions are not executed instantaneously, what are the losses?
The maximum losses due to price change of the Perp/Underlying will be similar to the impermanent loss of the pool. Impermanent Loss is proportional to Liquidity Provided * Concentration of Liquidity * Time .
We can capture most of this toxic flow due to our wash trading bot being non-fee sensitive. The reason we would prefer approach 2 to approach 3 is that it takes less transactions and hence less time to execute. Less time=less exposure to impermanent loss.
What are the effects of changing liquidity amount/concentration?
More liquidity allows us to capture a greater proportion of the trading fees, and allows us to execute larger trades while staying within the +-0.1% price impact range. The downside is you suffer more impermanent loss per unit of time.
Tldr: We provide liquidity for a very small period of time and have a bot which wash trades continuously while avoiding trades that can be taken advantage over 0.1% price impact. We continue this until reaching our desired trading volume and then remove our liquidity.
Proposed Solution
This exploit would work on any Uni v3 pools which rewards liquidity based on the trade volume it facilitated. The logic of this is that the “value” to traders is that trading volume that goes through it. Most solutions involving policing workarounds will have a counter-exploit.
Change the formula to:
Liquidity mining rewards = Liquidity Amount * Concentration * Time (or blocks) Within Range
Orca and Crema which are examples of Uniswap v3 Clones on Solana with trading rewards programs have the above liquidity mining rewards formula which makes them immune from the wash trading exploit.
Can you keep the trading rewards system and simply ban users that wash trade?
This wont work. There are too many different ways to wash trade that are hard to distinguish from real trading activity.
There are no examples of Uni-v3 style pools which have a trading-volume based rewards system like Perpetual Protocol. However, there are examples of decentralized orderbook exchanges which try to ban wash traders. Wash traders almost always find other ways to spoof trading activity and avoid detection. An example is DeversiFi, a layer 2 CLOB.
From the DeversFi Discord:
In reality, they didn’t catch wash traders who made even basic attempts to stay under the radar. Someone who isn’t me created multiple wallets with each wallet trying slightly different variations of detection-resistant wash trading approaches to figure out their flagging algorithm. None of them were caught/banned.
Leave a comment