
⚡ Thor vs The Bifrost - DoS Case Study
TL;DR
- Vulnerability: Denial of Service (gas griefing / revert-in-loop).
- Impact: Loki jams the BifrostBridge, blocking all Asgardians from crossing (withdrawing funds).
- Severity: High
- Fix: Use pull-payments instead of looped mass payouts; avoid unbounded iterations.
🎬 Story Time
In Thor (2011), the Bifrost is the magical rainbow bridge that lets Asgardians travel across realms. But what if Loki clogs the Bifrost with his tricks, preventing anyone from traveling?
In Solidity, this is a Denial of Service attack: one malicious participant makes it impossible for others to withdraw or execute a function.
- BifrostBridge = the vulnerable treasury with a payout loop.
- Thor = wants to withdraw his rightful share.
- Loki = inserts a malicious contract to block the loop and strand everyone.
Actors / Roles
Actor | Role |
---|---|
BifrostBridgeVulnerable | the payout contract (victim). |
LokiMalicious | attacker contract that always reverts. |
Thor (EOA/test) | just a normal user stuck in the loop. |
All Files Available here.
📌 Vulnerable Contract - BifrostBridgeVulnerable.sol
// SPDX-License-Identifier: MITpragma solidity ^0.8.24;
/* The BifrostBridge pays Asgardians their Ether by looping through all citizens. Loki can jam the bridge by reverting in his fallback, blocking ALL payouts.*/
contract BifrostBridgeVulnerable { address[] public asgardians; mapping(address => uint256) public vaultOfAsgard;
/// @notice Asgardians send their Ether to the Bifrost function enterBifrost(address realmWalker) external payable { require(msg.value > 0, "Bifrost requires Ether toll"); if (vaultOfAsgard[realmWalker] == 0) { asgardians.push(realmWalker); } vaultOfAsgard[realmWalker] += msg.value; }
/// @notice Heimdall distributes Ether to all Asgardians (⚠️ Vulnerable) function openBifrost() external { for (uint256 i = 0; i < asgardians.length; i++) { address traveler = asgardians[i]; uint256 tribute = vaultOfAsgard[traveler]; if (tribute > 0) { // Loki can revert here and jam the bridge (bool sent, ) = payable(traveler).call{value: tribute}(""); require(sent, "Bifrost jammed!"); vaultOfAsgard[traveler] = 0; } } }}
NOTE
transfer
/send
forward only 2300 gas;call
forwards all gas. A malicious fallback can revert the whole transaction or consume huge gas.call
is necessary for sending to contracts that may require more gas but introduces this DoS risk.
WARNINGOne bad actor can block everyone by reverting in their fallback. Thor is stranded because Loki jammed the Bifrost.
🪄 Loki’s Malicious Contract
// SPDX-License-Identifier: MITpragma solidity ^0.8.24;
/// @title Loki jams the Bifrost by reverting on receivecontract LokiTrickster { receive() external payable { revert("Loki jams the Bifrost!"); }}
Exploit Flow:
- Loki enters the Bifrost (deposits).
- Heimdall (
openBifrost
) tries to send tribute. - Loki’s contract reverts → the entire bridge halts.
- Thor and others are stranded with their funds stuck.
Gas Analysis – Gas Griefing
- The vulnerable
openBifrost()
loop grows linearly with the number of Asgardians. - Even if Loki did not revert, a long list of Asgardians could exhaust gas, causing a transaction failure.
- Partial reverts during the loop waste all gas spent so far, effectively denying service.
- Key takeaway: DoS isn’t just about stolen funds - it can also be about making legitimate users’ transactions impossible.
Fixed Contract - BifrostBridgeFixed.sol
// SPDX-License-Identifier: MITpragma solidity ^0.8.24;
/// @title Safe Bifrost with resilient withdrawalscontract BifrostBridgeFixed { mapping(address => uint256) public vaultOfAsgard;
/// @notice Deposit Ether to Bifrost function enterBifrost() external payable { require(msg.value > 0, "Bifrost requires Ether toll"); vaultOfAsgard[msg.sender] += msg.value; }
/// @notice Withdraw Ether safely, preserving vault on failure function crossBifrostSafe() external { uint256 tribute = vaultOfAsgard[msg.sender]; require(tribute > 0, "No tribute to cross");
vaultOfAsgard[msg.sender] = 0;
(bool sent, ) = payable(msg.sender).call{value: tribute}(""); if (!sent) { // Restore balance so user can retry vaultOfAsgard[msg.sender] = tribute; } }}
Fixes applied:
- No looping over Asgardians.
- Each Asgardian (
crossBifrost
) claims their own tribute. - Loki can jam only himself, not the whole bridge.
Foundry Test - DoSBifrost.t.sol
// SPDX-License-Identifier: MITpragma solidity ^0.8.24;
4 collapsed lines
import {Test} from "forge-std/Test.sol";import {BifrostBridgeVulnerable} from "../src/BifrostBridgeVulnerable.sol";import {BifrostBridgeFixed} from "../src/BifrostBridgeFixed.sol";import {LokiTrickster} from "../src/LokiTrickster.sol";
contract DoSBifrostTest is Test { BifrostBridgeVulnerable bifrostVuln; BifrostBridgeFixed bifrostSafe; LokiTrickster loki; address thor = makeAddr("Thor");
function setUp() public { // Deploy contracts bifrostVuln = new BifrostBridgeVulnerable(); bifrostSafe = new BifrostBridgeFixed(); loki = new LokiTrickster();
// Fund users vm.deal(thor, 1 ether); vm.deal(address(loki), 1 ether);
// Deposit into vulnerable contract bifrostVuln.enterBifrost{value: 1 ether}(thor); bifrostVuln.enterBifrost{value: 1 ether}(address(loki)); }
function test_BifrostJammedByLoki() public { // Expect revert because Loki reverts in openBifrost vm.expectRevert(); bifrostVuln.openBifrost(); }
function test_ThorCrossesSafeBifrost() public { vm.prank(thor); bifrostSafe.enterBifrost{value: 1 ether}();
uint256 beforeBalance = thor.balance; uint256 beforeVault = bifrostSafe.vaultOfAsgard(thor);
vm.prank(thor); bifrostSafe.crossBifrostSafe();
// Assert Thor received funds assertEq(address(thor).balance, beforeBalance + beforeVault); // Assert vault cleared assertEq(bifrostSafe.vaultOfAsgard(thor), 0); }
function test_LokiCannotBlockThor() public { // Loki enters safe contract vm.prank(address(loki)); bifrostSafe.enterBifrost{value: 1 ether}();
// Loki tries to withdraw but reverts internally (simulated in crossBifrostSafe) vm.prank(address(loki)); bifrostSafe.crossBifrostSafe();
// Thor enters and withdraws safely vm.prank(thor); bifrostSafe.enterBifrost{value: 1 ether}();
vm.prank(thor); bifrostSafe.crossBifrostSafe();
// Assert Thor successfully received funds assertEq(address(thor).balance, 1 ether); assertEq(bifrostSafe.vaultOfAsgard(thor), 0); }
function test_MultipleUsersSafeWithdrawal() public { address lokiUser = makeAddr("LokiUser"); address jane = makeAddr("Jane");
vm.deal(lokiUser, 1 ether); vm.deal(jane, 1 ether);
// Deposits vm.prank(thor); bifrostSafe.enterBifrost{value: 0.5 ether}(); vm.prank(jane); bifrostSafe.enterBifrost{value: 0.3 ether}(); vm.prank(lokiUser); bifrostSafe.enterBifrost{value: 0.2 ether}();
// Withdrawals vm.prank(thor); bifrostSafe.crossBifrostSafe(); vm.prank(jane); bifrostSafe.crossBifrostSafe(); vm.prank(lokiUser); bifrostSafe.crossBifrostSafe();
// Assert vaults cleared assertEq(bifrostSafe.vaultOfAsgard(thor), 0); assertEq(bifrostSafe.vaultOfAsgard(jane), 0); assertEq(bifrostSafe.vaultOfAsgard(lokiUser), 0);
// Assert balances assertEq(address(thor).balance, 1 ether ); assertEq(address(jane).balance, 1 ether ); assertEq(address(lokiUser).balance, 1 ether ); }
function test_CannotWithdrawWithoutDeposit() public { vm.expectRevert("No tribute to cross"); bifrostSafe.crossBifrostSafe(); }}
Why it’s a Denial of Service
- Funds aren’t stolen; access is denied.
- One malicious actor can block all legitimate users from interacting with the contract.
- This is a classic DoS pattern: revert-in-loop or gas exhaustion.
Auditor’s Checklist
- Loops with unbounded iteration?
- External calls inside loops?
- Use of
require
on external transfers? - No pull-payment alternative?
- Gas analysis for potential griefing?
Recommendations
- Prefer pull-payments (
crossBifrost
) instead of mass payouts (openBifrost
). - Avoid loops with external calls.
- Simulate malicious recipients (like Loki) in tests.
- Analyze gas cost for large arrays and partial failures.
References & Inspiration (Updated)
-
MCU: Thor (2011) – Loki jams the Bifrost, blocking all crossings.
-
Historical & Recent DoS-type incidents:
1. Governance Token DoS (2018) – Reverting reward pool participants froze all withdrawals. 2. King of the Ether Throne (2016) – Malicious fallback froze throne ownership. 3. OWASP SC10:2025 – DoS Patterns – Reverting fallback or gas exhaustion in loops halts contracts.
-
OpenZeppelin PullPayment pattern.
How to Run Locally
# install Foundrycurl -L https://foundry.paradigm.xyz | bashfoundryup
# clone repo & testforge test -vv
Challenge: Restore the Bifrost’s Flow!
Challenge Name: Thor’s Hammer Defense
Description: Can you wield Mjolnir like Thor to stop a DoS attack on the Bifrost contract? Save Asgard’s bridge!
- Deploy the vulnerable
BifrostBridgeVulnerable.sol
on Sepolia (e.g., a contract with a loop-based payout vulnerable to gas exhaustion). - Execute a DoS attack using a Foundry test to block payouts (e.g., spam the loop with malicious entries).
- Implement a fix (e.g., pull-based withdrawals or gas limit checks).
- Share your fixed contract’s Sepolia address on X with
#TheSandFChallenge
and@THE_SANDF
. - Bonus: Post your gas usage logs!
- Top submissions get a chance to join our audit beta program!
Three Quiz Questions to Test Understanding
-
What makes BifrostBridgeVulnerable.sol vulnerable to a DoS attack?
a) Missing reentrancy protection
b) Incorrect access control settings
c) Public exposure of transaction data
d) Unbounded loop in the payout functionShow Answer
Answer: d) Unbounded loop in the payout function
Explanation: A loop processing many entries can exceed the block gas limit, blocking payouts. -
Which fix prevents a DoS attack on BifrostBridgeVulnerable.sol?
a) Adding a multi-signature validator
b) Using pull-based withdrawals
c) Increasing the contract’s ETH balance
d) Hiding transaction details in the mempoolShow Answer
Answer: b) Using pull-based withdrawals
Explanation: Pull-based withdrawals let users claim funds individually, avoiding loop-based gas limits. -
What is the primary impact of a DoS attack on BifrostBridgeVulnerable.sol?
a) Users cannot withdraw their funds
b) Funds are stolen from the contract
c) The contract’s price is manipulated
d) Transactions are reordered for profitShow Answer
Answer: a) Users cannot withdraw their funds
Explanation: The DoS attack blocks the payout function, freezing user funds.
NOTEEducational minimal reproduction. MCU analogy (Loki clogging Bifrost) makes it memorable, but reflects real-world DoS scenarios blocking legitimate users’ withdrawals or actions.
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