The TWAP That Lied: How current() lets a tax swap quote a manipulated window
How a permissionless sync() can refresh the anchor behind Pharaoh’s current() quote, why that shrinks the effective TWAP to a few seconds & how a protocol can end up sell tax tokens into a manipulated
Most oracle bugs are obvious in hindsight. Someone used a spot price, skipped a staleness check, or trusted a feed that was never meant for execution.
This one is quieter.
The protocol did try to use the pair oracle. It did set a slippage floor. It did ask the legacy token/WAVAX pair for a quote before selling its accumulated tax tokens.
That is exactly why the bug is easy to miss.
The problem is not the absence of an oracle.
The problem is that the protocol used current() as if it were a fixed historical TWAP.
This vulnerability was found and reported by Slayer Security. If your protocol needs a security audit, they are worth reaching out to.
The tax swap path
The protocol accumulates tax tokens inside the contract and sells them during normal transfers once a threshold is reached. In simplified form, the flow looks like this:
The intent is obvious: ask the pair what the tax tokens should be worth, then protect the sale with a minimum output.
That only works if the quote is anchored to history the attacker cannot cheaply reshape.
What current() actually measures
Pharaoh’s legacy liquidity docs describe the pair as a conventional AMM on Avalanche, and the pair source exposes several quote helpers. The important distinction is that quote() is described as more secure because it samples over a configurable granularity, while `current()` measures from the latest observation to the current block.
That difference matters.
If the latest observation is 30 minutes old, `current()` behaves like a long-window TWAP.
If the latest observation is 4 seconds old, current() behaves like a 4-second average.
The source logic makes that explicit: current() starts from lastObservation(), then compares it with the current cumulative prices and computes the average over the elapsed time since that observation.
So the function is not inherently wrong. It is just much easier to game than the name suggests.
Why sync() matters
The other detail is that sync() is permissionless.
On a normal Uniswap-style pair, that would not be alarming by itself. sync() usually just aligns reserves with balances. But on this pair, it also refreshes the observation buffer once enough time has passed.
That creates the opening.
Once the observation window has aged past the pair’s period size, anyone can call sync() and force a fresh observation into the oracle history. After that, current() no longer reads from a long historical window. It reads from the new anchor to now.
That is the whole trick.
The attack?
The sequence is simple:
1. Wait until the pair is old enough that sync() can write a new observation.
2. Call sync() to plant the new anchor.
3. Move the pair price.
4. Trigger the protocol’s tax-sale path with a dust transfer.
5. Let the protocol ask current() for a quote from the fresh anchor to now.
6. Have the protocol accept a slippage floor based on that shaped quote.
The only timing nuance is the same-block fallback in current(). If the protocol calls current() in the exact same timestamp as the newly written observation, the pair falls back to the previous observation. So the attacker wants the fresh observation, then at least one new timestamp, then the forced sale.
That detail does not save the design. It just makes the exploit require one extra block of patience.
Why the slippage floor fails
Once the quote comes back from a tiny, attacker-shaped window, the protection becomes theater.
The protocol still computes amountOutMin. It still looks defensive. But the minimum output is now derived from a price the attacker just helped define.
If the attacker pushed the tax token price down before the quote, the protocol’s sale clears at a worse rate than the team intended. If liquidity is thin enough, the attacker may be able to round-trip the position and profit from the mispriced treasury sale.
Whether the economics are profitable depends on liquidity, fees, and how much capital the attacker needs to move the pair.
The design flaw does not.
Why quote() would have been the safer primitive
Pharaoh’s own docs are the giveaway here. The pair exposes quote(), which averages across sampled prices over a user-defined granularity. That is much closer to what most engineers mean when they say TWAP.
current() is not useless.
It is just a poor choice when the caller needs a quote that resists same-pair manipulation.
If the protocol wanted a quote that was harder to shape with a fresh observation and a short wait, quote() was the better fit.
The lesson
The dangerous part is not “the protocol forgot to use an oracle.”
The dangerous part is that it used an oracle function whose effective window can be reset by anyone who can call sync() and then wait out the next timestamp.
That is why this bug is easy to miss in review. The code looks responsible:
- it asks the pair for a price,
- it sets a slippage floor,
- it sells through the expected router path.
But the safety margin only works if the quote is anchored far enough in the past that an attacker cannot cheaply repaint it.
Here, they can.
tl;dr
An attacker can use permissionless sync() to refresh Pharaoh’s oracle anchor, manipulate the pair price, and then trigger the protocol’s tax swap so current() returns a quote from a tiny, attacker-shaped window. The slippage floor still exists, but it is based on poisoned input.
In one line:
sync-> move price -> dust transfer -> short-window quote -> treasury sells badly
Vantage provides continuous domain security and frontend integrity monitoring for Web3 protocols. Catch the signal before the damage spreads.
Built by @__Raiders · web3sec.news




