The Deposit From Address Zero — JavaScript Bug Hunt

Modelled on the Qubit Finance bridge exploit (January 2022): the attacker called the bridge's token deposit path with a resource that mapped to the zero…

  • Language: JavaScript
  • Layer: Backend
  • Difficulty: Medium
  • Concepts: Security, Validation, Money
  • Modelled on: Qubit Finance · 2022
  • Visible tests: a real token deposit mints the same amount; the zero-address resource is refused and mints nothing
  • Reward: 50 XP for a complete fix

Briefing

Modelled on the Qubit Finance bridge exploit (January 2022): the attacker called the bridge's token deposit path with a resource that mapped to the zero address — the placeholder used for native ETH. Calling an address that holds no contract code "succeeds" on Ethereum without doing anything, so the transfer helper reported success, the bridge recorded a deposit that never happened, and bridged tokens were minted on the other chain without any funds behind them. Losses were widely reported at around $80 million.

This project is a reconstruction. chain.js behaves like a low-level EVM call, and bridge.js trusts safeTransferFrom not throwing as proof that the funds arrived.

Fix deposit so only real token contracts are accepted and a mint is recorded only for funds the bridge actually received.

Bug report

BUG-QBIT-0127 · Priority: Critical · Reported by: bridge monitoring

deposit(chain, registry, resourceId, from, amount, mints) must:

  • throw for an unknown resourceId or a non-positive amount
  • throw if the resource's token address is the zero address (chain.ZERO) or an address with no contract code (chain.hasCode(address) is false)
  • throw if the token transfer fails
  • throw unless the bridge's balance of the token (chain.balanceOf(token, BRIDGE)) rose by exactly amount (fee-on-transfer tokens are unsupported)
  • on success, push exactly one { to: from, resourceId, amount } onto mints

Whenever it throws, nothing is pushed onto mints.

Observed: a deposit against the ETH resource (token = zero address) mints xETH on the other chain although no funds moved.

Logs

[bridge] Deposit resource=ETH token=0x0000000000000000000000000000000000000000 amount=190000000000000000000
[bridge] mint qXETH to 0xd01a... amount=190000000000000000000
[monitor] bridge ETH balance unchanged after deposit

The code as shipped

src/bridge/bridge.js (editable)

var safe = require("./safe");

var BRIDGE = "0xb41d6e";
exports.BRIDGE = BRIDGE;

// Locks `amount` of the resource's token in the bridge and records a mint on
// the other chain.
exports.deposit = function (chain, registry, resourceId, from, amount, mints) {
  var token = registry[resourceId];
  if (token === undefined) throw new Error("unknown resource " + resourceId);
  if (!(amount > 0)) throw new Error("amount must be positive");
  safe.safeTransferFrom(chain, token, from, BRIDGE, amount);
  mints.push({ to: from, resourceId: resourceId, amount: amount });
};

Read-only context: src/bridge/chain.js, src/bridge/safe.js.

Open the hunt to edit the files, run the visible tests and submit against the hidden ones. More JavaScript bug hunts.