Logo
Overview
Thor vs The Bifrost - Denial of Service (DoS) in Solidity (Gas Griefing Case Study)

Thor vs The Bifrost - Denial of Service (DoS) in Solidity (Gas Griefing Case Study)

December 12, 2025
4 min read

⚡ Thor vs The Bifrost - Denial of Service (DoS) in Solidity (Gas Griefing Case Study)

Important

Vulnerability: Denial of Service via gas griefing / revert-in-loop
Impact: A single malicious actor (Loki) can block all legitimate users from withdrawing funds
Severity: High
Fix: Pull-payment pattern (users claim individually) → no loops, no griefing


🎬 Story Time

In Thor (2011), Loki destroys the Bifrost so no one can leave Asgard.
Here, Loki doesn’t destroy it - he just clogs it with infinite mischief so no one else can cross.

The vulnerable contract tries to pay everyone in one transaction (push-payment).
Loki deploys a contract that reverts on receiving ETH → the whole loop reverts → Thor and every other Asgardian are stuck forever.

Classic DoS, just like real-world incidents (King of the Ether Throne, some governance reward pools, etc.).

Actors

ActorRole
BifrostBridgeVulnerableVictim contract with looped payout
LokiTricksterMalicious contract that reverts on receive
Thor (EOA)Legitimate user who just wants his ETH

All source code is available in the GitHub repo.

📌 Vulnerable Contract

BifrostBridgeVulnerable.sol
contract BifrostBridgeVulnerable {
address[] public asgardians;
mapping(address => uint256) public vaultOfAsgard;
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;
}
// ⚠️ The dangerous loop
function openBifrost() external {
for (uint256 i = 0; i < asgardians.length; i++) {
address traveler = asgardians[i];
uint256 tribute = vaultOfAsgard[traveler];
if (tribute > 0) {
// Loki reverts here → entire tx fails
(bool sent, ) = payable(traveler).call{value: tribute}("");
require(sent, "Bifrost jammed!");
vaultOfAsgard[traveler] = 0;
}
}
}
}
Warning

One malicious contract in the array = everyone is permanently locked out.
Thor is worthy… but even Mjolnir can’t fix a reverted transaction.

🪄 Loki’s Malicious Contract

LokiTrickster.sol
contract LokiTrickster {
receive() external payable {
revert("Loki jams the Bifrost!");
}
}

Exploit steps

  1. Loki enters the Bifrost (deposits).
  2. Heimdall (openBifrost) tries to send tribute.
  3. Loki’s contract reverts → the entire bridge halts.
  4. Thor and others are stranded with their funds stuck.

DoS Exploit Flow

Gas Griefing (Even Without Reverts)

Even if Loki is polite, the loop still grows forever → will eventually hit block gas limit → becomes impossible to call.
Partial executions waste huge gas with zero progress. Real DoS doesn’t need theft - just denial.

Fixed Version - Pull Payment Pattern

BifrostBridge_Safe.sol
contract BifrostBridge_Safe {
mapping(address => uint256) public vaultOfAsgard;
function enterBifrost() external payable {
require(msg.value > 0, "Bifrost requires Ether toll");
vaultOfAsgard[msg.sender] += msg.value;
}
function crossBifrostSafe() external {
uint256 tribute = vaultOfAsgard[msg.sender];
require(tribute > 0, "No tribute to cross");
vaultOfAsgard[msg.sender] = 0; // optimistic clear
(bool sent, ) = payable(msg.sender).call{value: tribute}("");
if (!sent) {
// restore so user can retry later
vaultOfAsgard[msg.sender] = tribute;
}
}
}

Loki can only grief himself. Everyone else crosses safely.

Foundry Test (with collapse for brevity)

DoSBifrost.t.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);
}
}

(Full test file in the repo - it proves Loki cannot block Thor in the fixed version.)

Auditor’s Checklist

Warning (Quick Audit Checklist)
  • Unbounded loops over user-controlled arrays?
  • External calls (call, transfer, send) inside loops?
  • Push payments instead of pull?
  • No gas limit checks or pagination?
    → If any yes → high DoS risk

Recommendations (Best Practices)

  • Use pull over push (OpenZeppelin PullPayment or manual)
  • Avoid unbounded loops - paginate or use mappings only
  • Test with malicious contracts that revert or consume max gas
  • Consider withdraw pattern + balance tracking

Quiz Time

Exercise (Question 1: What makes the vulnerable contract susceptible to DoS?)

a) Missing reentrancy guard
b) Incorrect access control
c) Unbounded loop with external call in payout function
d) Using msg.sender instead of tx.origin

Answer

Answer: c) Unbounded loop with external call
Explanation: A single malicious recipient can revert and block everyone forever.

Exercise (Question 2: Which pattern completely fixes this DoS vector?)

a) Add a reentrancy guard
b) Use transfer instead of call
c) Pull-payment (users claim individually)
d) Add a timelock

Answer

Answer: c) Pull-payment
Explanation: No loop = no griefing surface. Each user withdraws independently.

Exercise (Question 3: What is the main impact of this attack?)

a) Funds are stolen
b) Users cannot withdraw their funds
c) Contract upgrades itself
d) Owner can drain funds

Answer

Answer: b) Users cannot withdraw their funds
Explanation: Funds remain in contract but become unreachable due to revert.

Tip

All code shown is in the GitHub repository. Feel free to fork and play with it!