Replay Ethereum transactions with patched contract bytecode. Built for verifying security patches by re-executing historical transactions against fixed code while preserving as much on-chain context as possible.
PonDeReplay lets you:
- Replay historical transactions on pre-transaction state (block (N-1))
- Patch contract bytecode via
eth_callstate overrides or a local Anvil fork - Verify patches — block the attack tx while benign history still passes
- Sanity-check the replay itself — original bytecode must reproduce chain behaviour before patch results are trusted
- Replay with original bytecode (
on_original) — confirm success/revert matches the chain - Replay with patch bytecode — attack should revert; benign txs should still succeed
- Use
compare-patchfor a single attack tx, orreplay-historyfor many txs
- Python 3.9+
- Ethereum RPC endpoint (
ETH_RPC_URL) - Foundry (
anvil) — optional but recommended for same-block setup and timestamp-sensitive replays
git clone https://github.com/Deffensive/PonDeReplay.git
cd PonDeReplay
pip install -e ".[dev]"cp .env.example .env
# Edit .env — at minimum:
export ETH_RPC_URL="https://api-ethereum-mainnet.n.dwellir.com/YOUR_API_KEY"
export ETHERSCAN_API_KEY="..." # optional; needed for tx-list / replay-history via EtherscanPatch replay (gas bump on Anvil is on by default when --bytecode-file is set):
pondereplay replay \
--tx-hash 0xYOUR_TX \
--contract-address 0xCONTRACT \
--bytecode-file ./patch.hex \
-vOriginal bytecode sanity replay (omit --bytecode-file, or pass original.hex):
pondereplay replay \
--tx-hash 0xYOUR_TX \
--contract-address 0xCONTRACT \
-vReplays execute on state at block (N-1) (the block before the original tx).
pondereplay compare-patch \
--tx-hash 0xATTACK_TX \
--contract-address 0xCONTRACT \
--bytecode-file ./patch.hex \
--original-bytecode-file ./original.hex \
--attack-tx \
-vExpect classification effective_patch when the attack reverts under patch but succeeded under original.
Revert/success status alone is incomplete: a benign tx can still succeed under the
patch while changing storage/logs/balances, and an attack can be blocked in a sub-call
the attacker swallows while the top-level tx still returns success. Add --compare-state
to compare the actual on-chain state effect of the patched vs. original replay:
pondereplay compare-patch \
--tx-hash 0xTX --contract-address 0xCONTRACT \
--bytecode-file ./patch.hex --original-bytecode-file ./original.hex \
--compare-state -vThis forces the Anvil tier (the fast eth_call tier produces no state diff), captures a
prestateTracer diff for each replay, and:
- runs the reproduction check (O vs live;
reproduces_chain) — if the original replay does not structurally reproduce the chain tx, the comparison isinconclusive; - runs the preservation test (O vs P;
state_equivalent) on storage/logs/non-gas balances, with gas-derived state normalized out and time/block-derived state context-gated.
New classification outcomes:
| Outcome | Meaning |
|---|---|
needs_inspection |
Benign tx: both succeed, but the patch changed the state effect |
effective_patch (attack, sub-call) |
Top-level still succeeds, but the exploit's state effect is gone |
See docs/architecture.md for the architectural overview (how
replays are done, how/what we compare, with diagrams), and
docs/tx-replay-comparison.md for the full comparison
design — state effects, drift tolerance, verdicts, and block.timestamp handling.
| Command | Purpose |
|---|---|
replay |
Single tx with optional patched bytecode |
compare-patch |
Original + patch on one tx, with classification |
sanity-check |
Original bytecode only; compare to chain receipt |
replay-history |
Batch replay from Etherscan or a tx list file |
tx-list |
Export contract tx history from Etherscan to JSON |
bytecode |
Fetch current bytecode for a contract |
trace-analyze |
Inspect a transaction trace |
pondereplay --help
pondereplay replay --helpPonDeReplay uses a tiered strategy:
| Tier | When | Mode |
|---|---|---|
| Fast | No same-block setup issue | eth_call at (N-1) |
| Same-block override | Contract created earlier in block (N) | eth_call_same_block_override |
| Anvil strict | Timestamp / context mismatch or --strict-anvil |
anvil_indexed_strict |
Preflight runs before each replay: block index, same-block contract creations, time/context warnings, and whether escalation is needed.
Useful flags:
| Flag | When to use |
|---|---|
--use-anvil |
Prefer Anvil when same-block setup is required |
--strict-anvil |
Force timestamp-aligned Anvil replay |
--bump-gas-for-patch |
Re-estimate gas on Anvil ( default on when patch bytecode is provided; use --no-bump-gas-for-patch to disable) |
--auto-strict-on-mismatch |
Escalate when fast replay disagrees with chain (default: on) |
--bytecode-hex |
Provide deployed bytecode directly instead of via --bytecode-file |
--with-trace |
Run trace-backed preflight analysis (slower; off by default) |
--fork-url / ETH_FORK_URL |
Use a separate fork URL when Anvil should not fork from ETH_RPC_URL |
Timestamp-sensitive replays are covered in docs/tx-replay-comparison.md §6.
pondereplay replay-history \
--contract-address 0xCONTRACT \
--tx-list-file txs.json \
--bytecode-file patch.hex \
--attack-tx 0xATTACK... \
--output json \
-vtx-list-file accepts one hash per line, or JSON ["0x...", ...] / {"tx_hashes": [...]}.
Without --bytecode-file, each tx is replayed with on-chain bytecode at its own (N-1).
JSON results include execution outcome fields (independent of replay machinery success):
{
"onchain_reverted": false,
"local_reverted": true,
"revert_message": "execution reverted: only governance can call earn()",
"local_failure_reason": "revert_other",
"faithful_to_chain": true,
"replay_mode": "anvil_indexed_strict_patched",
"diagnostics": {
"faithfulness": "faithful",
"warnings": []
}
}| Field | Meaning |
|---|---|
onchain_reverted |
Did the tx revert on chain? |
local_reverted |
Did the local replay revert? |
faithful_to_chain |
Does local status match chain? (replay sanity) |
success |
Backwards-compatible alias for replay faithfulness, not "tx succeeded" |
local_failure_reason |
out_of_gas, patch_guard, revert_other |
diagnostics.faithfulness |
faithful, approximate, or unfaithful |
on_original (experiment soundness): per-tx check that original-bytecode replay matches chain before attributing failures to the patch.
- Raw hex (
0x6080…or without prefix) - Foundry / Hardhat / Truffle JSON artifacts
- Raw
.binfiles
from pondereplay import TransactionReplayer
replayer = TransactionReplayer(
"https://api-ethereum-mainnet.n.dwellir.com/YOUR_KEY",
bump_gas_for_patch=True,
)
result = replayer.replay_transaction(
tx_hash="0x...",
contract_address="0x...",
new_bytecode="0x...",
verbose=True,
)
orig, patched, report = replayer.replay_original_and_patched(
tx_hash="0x...",
contract_address="0x...",
patched_bytecode="0x...",
original_bytecode="0x...",
is_attack_tx=True,
)
print(report["classification"])pip install -e ".[dev]"
pytest tests/
pytest tests/ --cov=pondereplay --cov-report=html
black pondereplay/ tests/MIT — see LICENSE.
Issues and PRs welcome.