Reproduced Exploit
Valkyrie — a Bunni swap reverts and 1000 IN stays with the user
ValkyrieHooklet.afterSwap is onlyBunniHub. Bunni invokes that hook from BunniHook, not from BunniHub. The pool in this reduction is that caller. require(msg.sender == bunniHub) reverts. The pool catches the failure, refunds the input, and does not transfer output.
Chain
Other
Category
access-control
Date
Mar 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: 61559-swaps-are-not-possible-on-pools-registered-with-bunni-due-to. Standalone Foundry PoC and full write-up: 61559-swaps-are-not-possible-on-pools-registered-with-bunni-due-to_exp in the
evm-hack-registrymirror.
Vulnerability classes: vuln/access-control/broken-logic · vuln/dos/griefing · vuln/logic/wrong-condition
Reproduction: a self-contained Foundry PoC that compiles and runs offline with only
forge-std— no fork and no RPC. Trace: output.txt. PoC: test/61559-swaps-are-not-possible-on-pools-registered-with-bunni-due-to_exp.sol.
Key info#
| Impact | HIGH — the swap reverts. The user keeps 1000 IN and receives 0 OUT |
| Protocol | Paladin Valkyrie ValkyrieHooklet |
| Vulnerable code | afterSwap gated by onlyBunniHub |
| Bug class | The hook checks the hub, but Bunni calls it from the hook |
| Finding | Cyfrin (Draiakoo) — Paladin Valkyrie, 12 March 2025 · id 61559 |
| Report | 2025-03-12-cyfrin-paladin-valkyrie-v2.0.md |
| Source | AuditVault finding |
| Status | Fixed in Valkyrie commit 333f26b (onlyHook). Reproduced here as a local synthetic. |
| Compiler | solc 0.8.27, cancun, via IR |
TL;DR#
ValkyrieHooklet.afterSwap is onlyBunniHub. Bunni invokes that hook from BunniHook, not from BunniHub. The pool in this reduction is that caller. require(msg.sender == bunniHub) reverts. The pool catches the failure, refunds the input, and does not transfer output.
The user swapped 1000 IN. They still hold 1000 IN. They received 0 OUT. The 1000 OUT inventory stays in the pool.
The fixed hook is onlyBunniHook and is bound to the pool. The same swap returns true and pays the user 1000 OUT.
Vulnerable code#
modifier onlyBunniHub() {
// @> VULN: BunniHook is the caller, so this hub check reverts every swap
require(msg.sender == bunniHub, "hub");
_;
}
function afterSwap() external onlyBunniHub returns (bytes4) {
return ValkyrieHooklet.afterSwap.selector;
}
The fix named in the report is onlyBunniHook (later onlyHook) on afterSwap:
modifier onlyBunniHook() {
require(msg.sender == bunniHook, "hook");
_;
}
The pool is the local stand-in for Bunni's hook caller. No live Bunni deployment is required.
Attack walkthrough#
- The user is minted 1000 IN and 1000 OUT. The 1000 OUT is funded into the pool so a successful swap has inventory.
pool.swap(1000 ether)pulls 1000 IN and callshook.afterSwap().msg.senderinside the hooklet is the pool.bunniHubis a different contract. The require reverts.- The pool refunds 1000 IN and returns
(false, 0). - The user holds 1000 IN and 0 OUT. The pool still holds 1000 OUT.
Control: setBunniHook(pool) then the same fund and swap. afterSwap accepts the pool. The user receives 1000 OUT and their IN balance is 0.
Diagrams#
Impact#
Swaps on a pool configured with this hooklet cannot complete. A swap of 1000 IN is refunded. The user receives no output. The fixed check lets the same swap deliver 1000 OUT.
Remediation#
Gate afterSwap on the Bunni hook that actually calls it, not on BunniHub. Valkyrie commit 333f26b adds onlyHook for that caller.
Sources#
Taxonomy#
severity/highvuln/access-control/broken-logic·vuln/dos/griefing·vuln/logic/wrong-condition
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 61559-swaps-are-not-possible-on-pools-registered-with-bunni-due-to_exp (evm-hack-registry mirror).
- AuditVault finding: 61559-swaps-are-not-possible-on-pools-registered-with-bunni-due-to.
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.