Reproduced Exploit

Perpetual: two Pyth prices pay 150 on a 100 deposit

updatePrice stores whichever Pyth price it is given, and nothing stops two updates in one transaction. The maker holds 1 base. At price 100 the pool is worth 100, so a 100 deposit mints 1 share against the existing 1 share. The same transaction then stores price 200. The pool is worth 300 and that…

Feb 2024•Other•oracle•3 min read

Chain

Other

Category

oracle

Date

Feb 2024

Source

AuditVault

EVM Playground

Source-level debugger — step opcodes and Solidity in sync

evm-hack-analyzer

The attack is replayed in an in-browser EVM preloaded with the exact dumped fork state. The execution tree shows every call; step by Solidity line or by opcode across all depths — source, Stack, Memory, Storage, Balances (native / ERC-20 / NFT), Transient storage and Return value stay in sync. Click a tree node, opcode, or source line to jump. No backend, no live RPC.

Loading fork state…

Source & credit. Reproduction of a public audit finding curated by AuditVault — the original finding: 32162-h-1-two-pyth-prices-can-be-used-in-the-same-transaction-to-a. Standalone Foundry PoC and full write-up: 32162-h-1-two-pyth-prices-can-be-used-in-the-same-transaction-to-a_exp in the evm-hack-registry mirror.


Vulnerability classes: vuln/oracle/spot-price · vuln/logic/price-calculation

Key info#

ProtocolPerpetual
FindingSherlock 2024-02-perpetual H-1, AuditVault 32162
Vulnerable codeOracleMaker.deposit share mint
Harm100 collateral deposited at price 100 withdraws 150 at price 200. Extra 50. One price returns 100
Chainlocal synthetic

TL;DR#

updatePrice stores whichever Pyth price it is given, and nothing stops two updates in one transaction. The maker holds 1 base. At price 100 the pool is worth 100, so a 100 deposit mints 1 share against the existing 1 share. The same transaction then stores price 200. The pool is worth 300 and that 1 share of 2 withdraws 150. A control that uses only price 200 mints 0.5 share and withdraws 100.

The vulnerable code#

SOLIDITY
function updatePrice(uint256 newPrice, uint256 publishTime) external {
    require(block.timestamp >= publishTime, "future");
    require(block.timestamp - publishTime <= MAX_AGE, "stale");
    price = newPrice;
}

uint256 priceNow = _getPrice();
uint256 vaultValueXShareDecimals = _getVaultValueSafe(priceNow) - amount;
uint256 amountXShareDecimals = amount;
// @> VULN: share mint uses the price stored in this transaction
shares = (amountXShareDecimals * totalSupply) / vaultValueXShareDecimals;

Both updates pass publishTime == block.timestamp. MAX_AGE is one hour. A block has one timestamp, so the check does not distinguish the two prices.

Root cause#

The share price is the last stored Pyth update, and deposit and withdraw read it independently. Nothing binds a deposit and a withdraw to the same update.

Preconditions#

The maker has 1 base and 1 share outstanding. The attacker has 100 collateral. Two signed prices, 100 and 200, are both fresh for this block. No whitelist is required. The maker is prefunded with 200 collateral so the withdrawal of 150 can be paid. That prefund is the pool reserve, not the attacker's profit.

Attack walkthrough#

  1. Store price 100 and deposit 100. Shares minted: 1. See output.txt.
  2. Store price 200.
  3. Withdraw the 1 share. Payout is 150.
  4. Extra collateral is 50.
  5. The control stores only 200, deposits 100, mints 0.5 share, and withdraws 100.

Diagrams#

flowchart TD A["updatePrice 100"] --> B["Deposit 100 mints 1 share"] B --> C["updatePrice 200 in the same transaction"] C --> D["Vault value is now 300"] D --> E["1 share of 2 withdraws 150"] E --> F["Extra collateral is 50"]

Impact#

The attacker deposits 100 and withdraws 150. Existing LPs lose the 50. Using one price for both calls returns exactly the 100 deposit.

Remediation#

Do not let one transaction store two prices for an LP deposit and a withdrawal. Require deposits and withdrawals to be made by a trusted relayer, which is the fix the report recommends.

How to reproduce#

BASH
cd 32162-h-1-two-pyth-prices-can-be-used-in-the-same-transaction-to-a_exp
forge test -vv

Sources#


Sources & further analysis#

Reproductions & code

Alerts & third-party analyses

  • Web3Sec X hacked database: search.
  • Rekt leaderboard: search.
  • Solodit incident search: search.

These dashboards index community alerts tweets, post-mortems, and independent write-ups. Reach them through the protocol name above to cross-check this reproduction against other analyses.