October 2, 2026
SOAS III — Hedge Sandwich with Two Signed Prices: Part II
When a pool oracle let’s you decide which truth to tell and when to tell it

By Bozhidar Bonev
6 min read
Part of the series — "Snail On A Slope"
Introduction
In the previous part we explored the hedge sandwich kitchen: a hedging perp dealer, a DEX pool and a price band that trusted the oracle to be spot on. The attacker depended on the staleness of the oracle, which he cannot control so it is fair to say that luck was an ingredient in that recipe. If you've missed that, I recommend reading the first part of the article as our dummy protocol was explained there. We are just going to expand a bit on it in this second part as we are going to look at the way modern pull based oracles work and how a design feature can end up shooting itself in the foot.
Pull Oracles In A Nutshell Contrary to the more popular push oracles, pull oracles do not push a price to a contract at an interval. Instead, they form the prices and sign them off-chain at a specific moment, allowing anyone to submit them on-chain with the condition that they can't be older than the ones already submitted. This means that at any given moment there can be multiple, valid, different, true prices that can be used on-chain by anyone, at any "still fresh" time. So one asset, different true, valid prices and one decision on which price to use and when exactly.
Understanding the Protocol
Our dummy protocol consists of five contracts, explained in the previous part:
- MarginLedger
- SpotPool
- HedgingDealer
- PerpEngine
- PriceOracle
But with an extra new one:
- OracleDealer
Fast recap on the PriceOracle dummy contract: it is a simple implementation of a pull type price adapter like Pyth. It accepts prices through the setPrice() method by checking the publish time. In a real-world scenario it would also check the signatures but that is irrelevant for our purpose.
[Embedded content: b1d7e4e244a61bd5e69912268be1b308]
In the protocol we had a HedgingDealer, which used to take the opposite side of the user's perp position and hedge it on a DEX. Here we are introducing another dealer, the OracleDealer. which represents another way for users to open a perp position in the same protocol and for LPs to participate. The OracleDealer mechanism is much simpler and looks much safer as it fills user's orders at the current oracle price and takes the other side of the perp, no swaps on a DEX, no spread, no slippage. Moreover, it is not supplied by a fixed pile of liquidity but rather by LPs who mint and redeem shares with a price based on the total worth of the vault. Basically, a standard ERC-4626 style accounting, nothing new under the sun.
[Embedded content: 0ba72756ee3f16311eb4d963ca33eaed]
An example on the LP flow to make things completely clear:
- Alice deposits $50 000 through OracleDealer in order to become LP, since she is the first depositor she gets 50 000 shares, one dollar per share at start
- Some users' perp positions moved in favor of the vault so now the total worth of it has risen to $110 000
- Bob deposits $50 000, the same as Alice, but he only gets 50 000 * (50 000 / 110 000) which is approximately 22 720 shares
The share price at any given moment is determined by the vault value divided by the total shares in existence. The more the value of the vault grows, the more expensive the shares.
Anatomy Of The Second Attack — Two Signed Prices
You can probably already guess the attack, but here is the whole summary of it: When the share price of the vault is computed on each call to deposit() and withdraw() based on whatever the oracle states as "the current, valid" price and the attacker can decide what that "current, valid" price is, nothing stops him from from submitting a favorable price between a deposit() and a withdraw() without ever having to carry the risk associated with the anticipation of a price move. It's a sure thing so to say.
**Consider the following attack sequence: **Setup:
- An LP exists with a deposit of $100 000 as a first depositor, so 100 000 shares were given to him
- Some non-malicious trader opened a short perp position through the OracleDealer for 10 ETH at $4000 ETH current price. The dealer took the opposite side of the trade so now he has a 10 ETH long perp and as the fill happens exactly at the current price, the vault is still worth $100 000, nobody is up or down yet
- ETH is a crypto currency, duh, so it moves to $4010 per ETH. Somewhere off-chain a fresh price is signed that reflects the change but nobody has pushed it on-chain yet
Attack path:
- Attacker calls OracleDealer.deposit() with $1 000 000 while the new price has not been pushed on-chain yet. The vault is still $100 000 for 100 000 shares so, naturally, the attacker receives 1 000 000 shares
- Attacker calls PriceOracle.setPrice() with the new signed $4010 price of ETH
- Attacker calls OracleDealer.withdraw() of a 1 000 000 shares. The vault is now worth $1 100 000 + the 10 ETH long perp position taken against the non-malicious user, which currently yields an upside of $100. So the total vault value currently sits at $1 100 100 against 1 100 000 shares and an attacker withdraw in that case results in 1 000 000 * (1 100 100 / 1 100 000) = $1 000 090.91
As you can see, the attacker walks away with 90.9% of a price move to which he exposed himself for a single transaction. The actual risk taker, the LP, who is open to the winds of the possibly swinging 10 ETH long perp, gets to keep only 1/10th of the profit. Contrary to the previous part, there is no optimization issue here, the returns from this attack scale almost linearly with a asymptotic crawl from 90% to 100%.
You can run the same sequence with $10 000 instead of $1 000 000 and check it for yourself. There is no AMM curve to fight or fees and slippage to worry about. So this is the perfect place to use a flash loan, something that we saw was useless in the hedge sandwich attack. Moreover, the attack can be done in both directions — decreasing or increasing price.
Some limitations exist though:
- There has to be an existing, or many, perp position/s to which the dealer has taken the opposite side
- The dealer needs to have some perp position taken, someone needs to have opened
- The total profit from the attack is determined by position size * price move — the attacker can't profit more per unit than the price difference and the capital size only determines what fraction of the profit he is going to capture, approaching 100% but never actually reaching it with the increase of capital, as shown in the graph above
- Same as in the attack in part I, all has to happen in a single transaction, atomically
So who is paying for the lunch this time? In part I we saw that the dealer always netted to zero as he hedged the opposite perp position and they canceled by default and the loss appeared as bad debt falling on the LPs. Similarly, here the loss falls on the LPs holding shares in the vault but this time it can't be remedied in any way, there is no possible way out in any way, the protocol can't write it off as bad debt as when the attacker's withdraw executes, the loss is fully realized.
Conclusion of Part II
Well, first of all, we should be careful where we put our money, LPs get screwed constantly and not in the good way. Secondly, we should be aware of these small but sneaky ways for oracle tinkering that allow full drainage of a protocol. It was created as a different approach to delivering price on-chain but an implementation that is not careful can open the gates for an enormous theft. TWAP can always be considered as an option or even breaking the atomicity between depositing and withdrawing with a cooldown so that prices have time to be pushed on-chain.
So now we close our inspection of the financial kitchen with this second part, we stated last time that it is surely not getting a Michelin star but this time, at least, there was no cover charge on the entrance to the attack. If you lasted this far, go out in the sun and get an egg&cheese sandwich, ser, you deserve it, see you on the next step up the slope!