Reproduced Exploit
GachaTickets — swap quotes the reserves in the wrong order
getReserves returns (reserve0, reserve1) with token0 the lower address. _swap names those words (wethReserve, tokenReserve) without checking token0. In this pair the ticket is token0, so the payment reserve is 1000 and the ticket reserve is 100, but the quote uses them backwards. getAmountOut(10, 1…
Chain
Other
Category
logic
Date
Jan 2025
Source
AuditVault
EVM Playground
Source-level debugger — step opcodes and Solidity in sync
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.
Source & credit. Reproduction of a public audit finding curated by AuditVault — the original finding: 53301-c-01-incorrect-reserve-order-in-swap-pashov-audit-group-non. Standalone Foundry PoC and full write-up: 53301-c-01-incorrect-reserve-order-in-swap-pashov-audit-group-non_exp in the
evm-hack-registrymirror.
Vulnerability classes: vuln/logic/price-calculation · vuln/logic/incorrect-order-of-operations
Key info#
| Protocol | GachaTickets |
| Finding | Pashov Audit Group C-01, AuditVault 53301 |
| Vulnerable code | GachaTickets.swap reserve binding |
| Harm | Output 90661089388014913158. Fair output 987158034397061298. |
| Chain | local synthetic |
TL;DR#
getReserves returns (reserve0, reserve1) with token0 the lower address. _swap names those words (wethReserve, tokenReserve) without checking token0. In this pair the ticket is token0, so the payment reserve is 1000 and the ticket reserve is 100, but the quote uses them backwards. getAmountOut(10, 100, 1000) pays 90.661 ticket tokens. The sorted quote getAmountOut(10, 1000, 100) is 0.987. The control swap sends that fair amount.
The vulnerable code#
(uint256 wethReserve, uint256 tokenReserve,) = pair.getReserves();
if (wethReserve == 0 || tokenReserve == 0) revert InvalidPair();
// @> VULN: reserve0 is used as the payment reserve even when token0 is the output token
maxTokens = getAmountOut(cost, wethReserve, tokenReserve);
The fix reads token0 and assigns wethReserve from reserve0 only when token0 is the payment token.
Root cause#
Uniswap V2 reserve order is address order, not the order of the tokens the caller cares about. Binding getReserves() positionally to payment-then-token is correct only when the payment token is token0.
Preconditions#
The ticket token is token0, so reserve0 is the ticket reserve (100) and reserve1 is the payment reserve (1000). The swap input is 10 of the payment token. The pair holds the ticket tokens it sends out.
Attack walkthrough#
- Confirm token0 is the ticket, not the payment token.
swap(10e18)reads reserves as(100e18, 1000e18)and treats the first as payment.- The pair transfers
90661089388014913158ticket tokens to the buyer. - The fair constant-product output is
987158034397061298. swapSortedpays that fair amount to the tickets contract.
Diagrams#
Impact#
The protocol records and transfers 90.661 tickets for a trade whose fair output is 0.987. The extra 89673931353617851860 comes out of the pair. When the inverted quote is used as a minimum instead, the real pool swap reverts and buys are stuck. Sorting by token0 restores the 0.987 output.
Remediation#
uint256 wethReserve = token0 == paymentToken ? reserve0 : reserve1;
uint256 tokenReserve = token0 == paymentToken ? reserve1 : reserve0;
How to reproduce#
cd 53301-c-01-incorrect-reserve-order-in-swap-pashov-audit-group-non_exp
forge test -vv
Sources#
- AuditVault finding: https://github.com/Auditware/AuditVault/blob/main/findings/53301-c-01-incorrect-reserve-order-in-swap-pashov-audit-group-non.md
- Pashov report: https://github.com/pashov/audits/blob/master/team/md/Gacha-security-review_2025-01-27.md
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 53301-c-01-incorrect-reserve-order-in-swap-pashov-audit-group-non_exp (evm-hack-registry mirror).
- AuditVault finding: 53301-c-01-incorrect-reserve-order-in-swap-pashov-audit-group-non.
Alerts & third-party analyses
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.