On August 19, 2026, an attacker drained 191,156 USDC from Allbridge’s CCTP router on Base by redeeming a Circle-style message that moved no real money. Three weeks earlier, a different attacker had already taken $1.65 million from the same protocol’s Solana pools. In between, a bridge linking the XRP Ledger to the Coreum chain lost roughly 199,916 XRP across 94 withdrawals in 97 minutes, and the Sandbox bridge minted 14.9 billion unbacked SAND tokens. None of these attacks cracked a cipher or broke elliptic curve math. Each one walked through a verification check that nobody had tested.
This tutorial shows you how to build that missing test suite yourself. You will set up a Foundry project, write a mock bridge receiver, and then attack it the way real hackers attacked Allbridge, Coreum, and Sandbox this year, before your own bridge contract ever touches mainnet. By the end you will have a working repository of forged-message tests, replay tests, fuzz tests, and a CI gate that blocks a merge if any of them fail.
This guide is written for developers and security engineers auditing their own contracts or a client’s contracts under a signed engagement. Point these techniques only at code you own or are authorized to test.
Add up just the four bridge incidents above and you get roughly $2.7 million pulled out through message-verification and replay bugs within about five weeks, spread across three separate protocols on three separate chains. None of those protocols were new or unaudited. They shipped, ran for months, and were exploited anyway by an attacker who found the one check the original review missed. That is the argument for building a dedicated, repeatable test suite instead of relying on a single audit report to catch everything once and for all.
Why Cross-Chain Bridges Keep Getting Drained in 2026
Bridge exploits share a pattern that keeps repeating: the receiving contract trusts a piece of data about another chain without confirming the economic event actually happened there. In the Allbridge case, the attacker called Circle’s MessageTransmitterV2.sendMessage on Polygon back on July 26 to construct a message claiming a 1 million USDC transfer, without ever burning that USDC. The message sat unused for 24 days. Once Allbridge’s Base router accumulated 191,156 USDC in real deposits, the attacker replayed the forged message through receiveCctpMessage, which lacked a check tying the claimed amount to an actual burn event, and paired it with an Aave flash loan to match the balance before withdrawing.
The Coreum-XRPL bridge failed differently but for a related reason: its verification layer let 94 separate withdrawal calls drain the bridge in under two hours without flagging the abnormal velocity. The Sandbox bridge minted SAND directly, meaning its mint function trusted a cross-chain signal that had no cap or reconciliation against locked collateral. August 2026 alone logged a record 50 crypto hacks industry-wide, even as total dollar losses fell roughly 49% from July, according to industry incident trackers, which tells you attackers are hitting smaller, more precise targets instead of one giant score.
Zoom out further and the pattern holds across the wider DeFi market, not just bridges. Q2 2026 set a record with 99 separate DeFi exploits and roughly $746 million lost across the sector, and lending markets built on cross-chain price feeds kept adding to that total through the summer, including a Moonwell price-oracle exploit on Base that cost around $8.7 million. Bridges and oracle-dependent lending markets share the same underlying weakness: both trust a data feed about conditions somewhere else, and both get exploited the moment that feed can be manipulated or forged without the receiving contract noticing. This is not a new class of bug in cryptography terms. It is an old class of bug (missing input validation) wearing a cross-chain costume, and it is still catching well-funded teams off guard in 2026.
The table below lines up the year’s bridge-specific incidents so you can see the shape of the problem before you start writing tests against it.
| Protocol | Date (2026) | Loss | Root cause |
|---|---|---|---|
| Allbridge (Base/Polygon CCTP router) | Aug 19 | ~$190,000 (191,156 USDC) | Forged CCTP-style message accepted without confirming a real burn |
| Allbridge Core (Solana pools) | Jul 19–20 | ~$1.65M | Flash loan manipulated pool exchange rate in a single atomic transaction |
| Coreum–XRPL bridge | Aug 9 | ~$200,000 (199,916 XRP) | Verification flaw allowed 94 withdrawals in 97 minutes |
| Sandbox bridge | Aug 21–23 | ~$675,000 extracted, $49B face value minted | Unbacked minting bug, no cap tied to locked collateral |
| Moonwell (Base) | 2026 | ~$8.7M | Price oracle manipulation on the lending side of a cross-chain deployment |
Every row in that table maps to a test you can write before deployment. The rest of this guide walks through building that test suite step by step.
Prerequisites: Tools and Versions You Need
You do not need a mainnet deployment or a live bridge to follow along. Everything below runs locally against a mock contract that mirrors the message-verification pattern used by real bridges. Install the following before you start.
| Tool | Version | Purpose |
|---|---|---|
| Foundry (forge, anvil, cast) | latest version via foundryup | Local EVM, fuzzing, mainnet forking |
| Solidity | ^0.8.24 | Contract language for the mock receiver and tests |
| Slither | latest version via pip | Static analysis pass on the finished contract |
| Node.js | 20 LTS | Optional, for scripting message payloads |
| Git | any recent release | Version control and CI integration |
If you already work in Solidity day to day, budget about 90 minutes to work through all 12 steps and build the full project. If Foundry is new to you, add another 30 minutes for the install and your first few compiler errors.
Scoping Your Bridge Security Review
Before you open an editor, write down what is actually in scope. A bridge security review can mean the message-passing contract alone, the token vault that locks and releases funds, the off-chain relayer or validator software, or all three together. Trying to cover all three in one pass usually means none of them get tested thoroughly, so pick a boundary and state it in writing at the top of your findings document.
A useful rule of thumb: if a bug requires compromising an off-chain signer’s private key, it belongs in a separate operational security review, not this Foundry suite. If a bug can be triggered purely by crafting a malicious on-chain message or transaction sequence, it belongs here. The Allbridge, Coreum, and Sandbox incidents this guide is built around all fall into that second category, which is exactly why they are testable with the tools in this tutorial rather than requiring a penetration test of someone’s laptop.
Step 1: Map the Bridge’s Trust Model and Message Flow
Before you write a single test, draw the message path on paper. Every cross-chain bridge falls into one of three verification models: externally verified (a third party like Circle or a validator set attests to the message), natively verified (the destination chain checks a Merkle proof against the source chain’s state), or optimistically verified (the message is accepted immediately and can be challenged within a fraud window).
Allbridge’s CCTP router falls into the first category. It trusted Circle’s attestation that a message was well-formed without independently confirming that the claimed USDC had actually been burned on the source chain. Write down, for your own bridge, exactly which category it falls into and which single data point it trusts blindly. That data point is your first test target.
- Which contract or off-chain service produces the attestation?
- What fields are actually included in the signed payload (chain ID, sender, receiver, amount, nonce)?
- What happens if two valid-looking messages arrive for the same nonce?
- Is there a cap on how much value a single message can move?
Step 2: Set Up the Foundry Test Environment
Install Foundry and scaffold a fresh project. Keep this repository separate from your main contract repo at first so you can iterate on attack scenarios without touching production code.
curl -L https://foundry.paradigm.xyz | bash
foundryup
forge init bridge-security-tests --no-git
cd bridge-security-tests
forge install OpenZeppelin/openzeppelin-contracts
forge build
Run forge --version to confirm the install worked. You should see a build date from 2026. If the command is not found, restart your shell so the foundryup PATH change takes effect.
Step 3: Build a Mock Bridge Receiver Contract
Write a small receiver contract that mirrors the shape of a real cross-chain message handler: a struct with chain ID, sender, receiver, amount, and nonce, plus a mapping that should block replay. This is your target. In a real engagement you would point these same tests at the client’s actual contract instead.
// src/MockBridgeReceiver.sol
pragma solidity ^0.8.24;
contract MockBridgeReceiver {
struct CrossChainMessage {
uint32 sourceChainId;
address sourceSender;
address destReceiver;
uint256 amount;
uint64 nonce;
}
mapping(bytes32 => bool) public processedMessages;
mapping(uint64 => bool) public usedNonces;
event MessageReceived(bytes32 indexed messageHash, uint256 amount);
function receiveMessage(CrossChainMessage calldata m, bytes calldata attestation) external {
bytes32 hash = keccak256(abi.encode(m));
require(!processedMessages[hash], "replayed message");
require(!usedNonces[m.nonce], "nonce reused");
require(_verifyAttestation(hash, m.sourceChainId, attestation), "bad attestation");
processedMessages[hash] = true;
usedNonces[m.nonce] = true;
emit MessageReceived(hash, m.amount);
}
function _verifyAttestation(bytes32, uint32, bytes calldata) internal pure returns (bool) {
// Swap in real signature or threshold verification here.
return true;
}
}
Notice that _verifyAttestation currently returns true unconditionally. That is deliberate: it is the exact shape of the gap that let the Allbridge attacker’s forged message through, and it gives you a contract that will fail every test you are about to write until you actually harden it.
Keep this mock as close as reasonably possible to your real bridge’s data shape. If your production contract signs over additional fields, such as a token address or a destination-specific fee, add those fields to the struct now rather than after you have already written a dozen tests against the simplified version. Rewriting the struct halfway through a test suite is one of the more time-consuming mistakes you can make in this workflow, since every test that constructs a message has to be touched again.
Step 4: Simulate a Forged Attestation Replay
Write the test that reproduces the core of the Allbridge exploit: a message that claims a large transfer happened on the source chain, with no economic event backing it. A hardened receiver should reject this outright.
// test/ForgedAttestation.t.sol
pragma solidity ^0.8.24;
import "forge-std/Test.sol";
import "../src/MockBridgeReceiver.sol";
contract ForgedAttestationTest is Test {
MockBridgeReceiver receiver;
function setUp() public {
receiver = new MockBridgeReceiver();
}
function testRejectsMessageWithNoRealDeposit() public {
MockBridgeReceiver.CrossChainMessage memory forged = MockBridgeReceiver.CrossChainMessage({
sourceChainId: 137,
sourceSender: address(0xBEEF),
destReceiver: address(this),
amount: 1_000_000e6,
nonce: 1
});
vm.expectRevert();
receiver.receiveMessage(forged, hex"");
}
}
Run forge test -vvv --match-test testRejectsMessageWithNoRealDeposit. Against the mock contract above, this test fails, because _verifyAttestation accepts anything. That failure is the whole point: it is proof, in a repeatable test, that your contract has the same hole Allbridge shipped with.
Step 5: Test Nonce and Message-Replay Protection
A correct receiver should reject the exact same message twice, and it should reject a message replayed on a different chain if the nonce and payload otherwise match. Both conditions get tested separately, because a common bug is protecting against one but not the other.
function testCannotReplaySameMessageTwice() public {
MockBridgeReceiver.CrossChainMessage memory m = _buildValidMessage(1);
receiver.receiveMessage(m, _validAttestation(m));
vm.expectRevert(bytes("replayed message"));
receiver.receiveMessage(m, _validAttestation(m));
}
function testCannotReuseNonceWithDifferentPayload() public {
MockBridgeReceiver.CrossChainMessage memory m1 = _buildValidMessage(2);
receiver.receiveMessage(m1, _validAttestation(m1));
MockBridgeReceiver.CrossChainMessage memory m2 = _buildValidMessage(2);
m2.amount = 999e6;
vm.expectRevert(bytes("nonce reused"));
receiver.receiveMessage(m2, _validAttestation(m2));
}
Output for a passing run looks like this once the receiver is hardened:
[PASS] testCannotReplaySameMessageTwice() (gas: 68421)
[PASS] testCannotReuseNonceWithDifferentPayload() (gas: 71984)
Test result: ok. 2 passed; 0 failed; finished in 3.12ms
Step 6: Test Chain ID and Contract-Address Binding
A message signed for Polygon should never be replayable on Base, even if every other field matches. This is the domain-separation check that EIP-712 solves for typed signatures, and it needs its own direct test rather than an assumption baked into the signature scheme.
function testCannotReplayOnDifferentChainId() public {
MockBridgeReceiver.CrossChainMessage memory m = _buildValidMessage(3);
bytes memory sig = _validAttestation(m);
m.sourceChainId = 8453; // swap Polygon (137) for Base (8453)
vm.expectRevert();
receiver.receiveMessage(m, sig);
}
If this test passes against your unmodified contract, stop and check whether your attestation verification actually includes chain ID in the signed hash, or whether it is checked separately and can be swapped out after the signature is already valid. That gap is subtle and easy to miss in a code review that only reads the function line by line.
Step 7: Fuzz the Message Decoder
Manual test cases catch the exploits you already know about. Fuzzing catches the ones you have not thought of yet, particularly around amount encoding, decimal mismatches between chains, and integer boundaries. Foundry’s built-in fuzzer runs a function hundreds of times with randomized inputs.
function testFuzz_DecoderRejectsOversizedAmount(uint256 amount, uint64 nonce) public {
vm.assume(amount > type(uint128).max);
MockBridgeReceiver.CrossChainMessage memory m = _buildValidMessage(nonce);
m.amount = amount;
vm.expectRevert();
receiver.receiveMessage(m, _validAttestation(m));
}
Run it with forge test --match-test testFuzz_DecoderRejectsOversizedAmount -vv. By default Foundry runs 256 randomized inputs per fuzz test. Bump that number to at least 10,000 with --fuzz-runs 10000 before you consider the decoder logic settled, since the default count can miss narrow edge cases that only show up one time in a few thousand.
Step 8: Test Validator Threshold and Signature Quorum Logic
If your bridge relies on a validator set instead of a single attester, the quorum logic itself needs direct tests, not just the individual signature check. Write tests for exactly-at-threshold, one-below-threshold, and duplicate-signer scenarios, since a common bug lets the same validator sign twice and count as two independent votes.
- A message with exactly the required number of unique valid signatures passes
- A message with one fewer signature than required is rejected
- A message with the same validator’s signature repeated to hit the count is rejected
- A message signed entirely by validators who have since been removed from the active set is rejected
Each of those four cases becomes its own test function in the same pattern as steps 4 through 6. Skipping the duplicate-signer case is one of the more common gaps found in bridge audits, because it looks correct at a glance if you only check signature count rather than unique-signer count.
Validator quorum bugs have a long history in this space. The 2022 Ronin bridge hack, which cost roughly $625 million, and the 2022 Wormhole exploit, which cost around $325 million, both traced back to weaknesses in how few signatures were actually required or checked, not to a broken cryptographic signature scheme. Four years later, quorum logic is still one of the highest-value targets in a bridge codebase, which is why it gets its own dedicated step in this guide rather than a single passing mention.
Step 9: Simulate a Flash-Loan-Assisted Drain
The Allbridge Core incident on Solana and the original Allbridge Base attack both used a flash loan to top up a balance right before the exploit fired, in a single atomic transaction. Test this by forking mainnet state and simulating the same sequence: borrow, trigger the vulnerable path, repay, and check the net balance change.
forge test --match-contract FlashLoanDrainTest --fork-url $MAINNET_RPC_URL -vvv
Inside that test, use a mock flash loan provider (or fork Aave directly) to borrow the exact amount the router is short, call your bridge’s withdraw or receive function, then repay the loan in the same test function. If the test ends with the attacker contract holding a positive balance, your receiver has the same structural weakness that cost Allbridge $1.65 million in July.
Flash loans do not create new bugs on their own. What they do is remove the capital requirement for exploiting a bug that was already there, which turns a theoretical weakness into a same-block, fully funded attack available to anyone with gas money. That is why a bridge that looks safe against an attacker with a modest starting balance can still fail badly once you test it against an attacker who can temporarily borrow tens of millions of dollars for the length of a single transaction.
Step 10: Test Pause Switches and Rate Limits
The Coreum-XRPL bridge lost its funds across 94 separate withdrawals over 97 minutes. A rate limit or an automatic pause triggered by abnormal withdrawal velocity would have capped the damage well before the bridge was empty. Test that your circuit breaker actually fires, and just as importantly, test that it cannot be bypassed by splitting a large withdrawal into many small ones.
function testPauseTriggersAfterVelocityThreshold() public {
for (uint256 i = 0; i < 50; i++) {
receiver.receiveMessage(_buildValidMessage(uint64(i)), _validAttestation(_buildValidMessage(uint64(i))));
}
assertTrue(receiver.paused(), "bridge should auto-pause after 50 rapid withdrawals");
}
If your contract has no paused() state at all, that is itself a finding worth writing down before you move to the next step.
Step 11: Run Slither Static Analysis
Foundry tests confirm behavior you already thought to check. Slither scans for patterns across the whole contract, including ones you did not think to write a test for, such as unchecked external calls or reentrancy in a function that touches token transfers.
pip install slither-analyzer
slither src/MockBridgeReceiver.sol --print human-summary
slither src/MockBridgeReceiver.sol --detect reentrancy-eth,unchecked-transfer,tx-origin,arbitrary-send-erc20
Treat every high and medium finding as something to either fix or explicitly document as an accepted risk with a written reason. Do not silently ignore a finding just because the exploit path looks unlikely. The Sandbox bridge's unbacked-mint bug also looked unlikely, right up until someone found the specific call sequence that triggered it.
Read the human-summary output carefully rather than skimming straight to the detector list. It shows you the contract's external attack surface, meaning every function an outside address can call directly, which is the same list an attacker starts from when probing a live deployment. If that summary includes a function you forgot was externally callable, add a test for it before moving on, since an untested public function is effectively an untested attack surface.
Step 12: Build a Regression Suite and CI Gate
Every test you wrote in steps 4 through 11 needs to run automatically on every pull request, not just once before launch. Wire Foundry and Slither into CI so a future code change cannot silently reopen a hole you already closed.
name: bridge-security-tests
on: [pull_request]
jobs:
forge-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: foundry-rs/foundry-toolchain@v1
- run: forge test -vvv --gas-report --fuzz-runs 10000
- run: pip install slither-analyzer && slither src/ --fail-high
With this in place, a pull request that removes a require statement, weakens the nonce check, or drops the chain ID from the signed payload fails CI automatically instead of shipping to mainnet.
Common Pitfalls When Testing Bridge Contracts
These are the mistakes that show up again and again in bridge audits, including in protocols that had already passed a formal review before they were exploited.
- Testing only the happy path. A suite that only proves valid messages work tells you nothing about what an attacker can get through. Every step above pairs a valid case with an attack case, and a suite that skips the attack case is not a security test suite, it is a functionality test suite wearing a security label.
- Trusting an attestation without checking the economics behind it. This is exactly the gap Allbridge shipped with: a valid-looking attestation that never corresponded to a real burn on the source chain.
- Fuzzing amounts but not decimals. Cross-chain transfers often move between tokens with different decimal precision (6 for USDC, 18 for most ERC-20s), and rounding errors at that boundary can be drained repeatedly at small scale until they add up to a meaningful loss.
- Ignoring the off-chain relayer or validator infrastructure. A contract-only threat model misses attacks on the signing service itself, which is outside Foundry's reach but still belongs in your written scope, even if it is handed off to a separate team.
- Testing exclusively on a local Anvil chain with no forked state. Flash-loan and liquidity-manipulation attacks often depend on real pool depth and real token balances that a blank local chain does not reproduce, so a suite that never forks mainnet will miss step 9 entirely.
- Hardcoding the attacker's role as an external address only. Some of the most damaging bridge bugs are only reachable from inside another contract, because they depend on reentering the bridge mid-call. Write at least one test where the caller is itself a smart contract, not an externally owned account.
- Skipping negative gas-limit and reentrancy checks on the pause function itself. A pause mechanism that can be front-run, delayed, or reentered before it takes effect provides no real protection, so it needs the same adversarial testing as every other function in the contract.
- Treating a passing test suite as a finished audit. A test suite proves the cases you thought to write. It does not replace a second set of eyes or a professional review before mainnet deployment, and no amount of green checkmarks changes that.
Troubleshooting Guide
Expect to hit a handful of these while building the suite above. The table below covers the ones that come up most often.
| Symptom | Likely cause | Fix |
|---|---|---|
forge: command not found | Shell PATH not updated after install | Restart your terminal or run source ~/.bashrc after foundryup |
| Fork tests hang or time out | Public RPC endpoint rate-limiting your fork calls | Use a dedicated RPC provider key and pass it via --fork-url |
vm.expectRevert() fails even though the call reverts | Revert reason string does not match, or call reverts one line earlier than expected | Use bare vm.expectRevert() first to confirm any revert, then narrow to the exact reason string |
| Fuzz test passes locally but fails in CI | Different random seed found an edge case CI's run didn't hit locally, or vice versa | Pin a seed for reproducibility with --fuzz-seed when debugging a specific failure |
| Slither throws on compilation | Solc version mismatch between Slither and your foundry.toml | Set solc_version explicitly in Slither's config to match your project |
| Out-of-gas errors only during fuzzing | Fuzzed inputs generate loops or arrays far larger than realistic production data | Bound fuzz inputs with vm.assume() to realistic ranges |
| Mock attestation always returns true | Placeholder verification function was never replaced with real logic | Swap in your actual signature or threshold verification before trusting any passing test |
| CI job passes but a manual run fails | Stale lib/ dependencies cached in the CI runner | Add forge install as an explicit CI step rather than relying on a cached checkout |
Advanced Tips for Production Bridge Audits
Once the core Foundry suite is green, a few additional layers close the gap between "passes my tests" and "survived a determined attacker."
Formal verification tools go further than fuzzing by proving a property holds for every possible input, not just the ones a fuzzer happened to generate. If your bridge handles enough value to justify the cost, a formal specification of the nonce and chain ID invariants is worth commissioning alongside the Foundry suite you just built.
Run an economic simulation of your worst-case scenario before launch: what is the maximum an attacker could extract if every check you have not tested turned out to be broken at once? That number should drive how much liquidity you allow the bridge to hold before a manual review checkpoint.
List your contracts on a bug bounty platform such as Immunefi before mainnet launch, and keep the reward scaled to a meaningful fraction of the funds at risk. A bounty that pays less than an exploit is worth gives a rational attacker no reason to disclose instead of drain.
Finally, monitor validator or attester key rotation in production. A quorum check that was correct on launch day can quietly become unsafe months later if enough signing keys change hands without the on-chain validator set being updated to match.
Write an incident response runbook before you need one, not during an active drain. Decide in advance who has the authority to trigger an emergency pause, how fast that authority can act at 3 a.m. on a weekend, and what evidence you need to collect before publishing a post-mortem. The Coreum-XRPL bridge lost its funds inside 97 minutes, which is not enough time to improvise a response process from scratch.
If your bridge design allows a message to trigger a callback into another contract, add a cross-chain reentrancy lock on top of the standard single-chain reentrancy guard. A contract can be safe against reentrancy within one chain's execution and still be vulnerable to a second message arriving mid-processing from a different chain, since standard reentrancy guards only track state within a single transaction context.
The Complete Working Project
By the end of the 12 steps above, your repository should look like this:
bridge-security-tests/
├── src/
│ └── MockBridgeReceiver.sol
├── test/
│ ├── ForgedAttestation.t.sol
│ ├── ReplayProtection.t.sol
│ ├── ChainIdBinding.t.sol
│ ├── DecoderFuzz.t.sol
│ ├── ValidatorQuorum.t.sol
│ ├── FlashLoanDrain.t.sol
│ └── PauseRateLimit.t.sol
├── .github/workflows/bridge-security-tests.yml
└── foundry.toml
Run the whole suite with a single command before every commit:
forge test -vvv --gas-report --fuzz-runs 10000
A clean run against a properly hardened receiver looks like this:
Ran 7 test suites: 14 tests passed, 0 failed, 0 skipped
Gas report saved. No high or medium Slither findings.
Every test in that suite maps directly back to a real 2026 incident: forged attestations from Allbridge, withdrawal velocity from the Coreum-XRPL bridge, and unbacked minting logic from the Sandbox exploit. That traceability is what turns a generic checklist into a test suite you can actually defend in a post-incident review.
The helper functions referenced throughout this guide, _buildValidMessage and _validAttestation, are intentionally left for you to implement against your own contract's actual signing scheme rather than hardcoded here. In the mock receiver above, a minimal version of _buildValidMessage just fills in a CrossChainMessage struct with a given nonce and safe defaults for everything else, while _validAttestation produces whatever byte string your real _verifyAttestation function expects once you have replaced the placeholder with real signature checking. Building those two helpers first, before writing the attack-specific tests, saves you from duplicating message-construction logic across all seven test files.
For a deeper checklist to run alongside these tests, the Spearbit bridge security checklist and the ComposableSecurity SCSVS bridge component checklist both cover verification-model-specific checks in more depth than a single tutorial can. The Foundry Book documents every cheat code used above, the OWASP Smart Contract Top 10 gives a broader vulnerability-class reference beyond bridges specifically, and Circle's own CCTP documentation is worth reading directly if your bridge integrates with it, since the Allbridge exploit hinged on a gap between what CCTP attests to and what a receiving contract actually needs to verify.
Frequently Asked Questions
Do I need a live testnet deployment to run these tests?
No. Every test in this guide runs against a local Foundry environment, either with Anvil's default state or a mainnet fork for the flash-loan scenario in step 9. You only need a testnet once you are ready to test the deployed bytecode itself.
Is Foundry better than Hardhat for this kind of security testing?
Foundry's built-in fuzzer and cheat codes (vm.expectRevert, vm.assume, mainnet forking) make attack simulation faster to write than in Hardhat, which needs additional plugins for equivalent fuzzing. Both frameworks can get you to the same test coverage in the end, but Foundry generally gets you there with less setup code and fewer JavaScript dependencies to maintain.
How many Foundry fuzz runs are enough before I trust a decoder test?
The default of 256 runs is fine for early development. Before treating a fuzz test as a real security signal, raise it to at least 10,000 runs with --fuzz-runs, and consider a dedicated invariant-testing pass for anything handling live value.
What made the Allbridge attack possible for 24 days before it was used?
The attacker constructed the forged message on July 26 and waited until the Base router held enough real USDC to make the withdrawal worthwhile. Nothing about the vulnerability itself required that delay. The forged message would have worked the moment it existed, which is why nonce and amount checks need to hold regardless of timing.
Does adding a pause function guarantee a bridge can't be drained?
No. A pause only helps if something actually triggers it before the funds are gone, which is why step 10 tests the velocity-based trigger directly rather than assuming the function exists and works. The Coreum-XRPL bridge had 97 minutes and 94 withdrawals to be caught and was not.
Should I run Slither before or after writing Foundry tests?
Either order works, but running Slither first often surfaces obvious issues (unchecked external calls, missing access control) that are cheaper to fix before you invest time writing detailed Foundry tests around a function you are about to rewrite anyway.
Can this test suite catch a validator key compromise?
Not directly. These tests assume the signing keys themselves are secure and focus on whether the contract logic correctly enforces quorum, chain binding, and replay protection given valid or invalid signatures. Key management and off-chain validator security need a separate operational review.
Is 12 steps enough for a real bridge audit, or just a starting point?
Treat this as a starting point that covers the specific bug classes behind 2026's biggest bridge exploits. A full audit for a bridge holding significant value should add a professional third-party review, a bug bounty, and the formal verification and economic simulation work covered in the advanced tips section above.
What is the fastest way to prioritize which step to start with if I have limited time?
Start with step 4 (forged attestation) and step 5 (replay protection) first, since those two map to the exact bug that cost Allbridge $190,000 and are usually the fastest to write against an existing contract. Step 9 (flash-loan simulation) takes the longest to set up because it requires a mainnet fork, so schedule it once the faster tests are already passing.




