The Index That Started at Zero — JavaScript Bug Hunt

Modelled on Compound's Proposal 062 (September 2021): an upgrade to the contract that distributes COMP rewards contained a bug in how reward indexes were…

  • Language: JavaScript
  • Layer: Backend
  • Difficulty: Hard
  • Concepts: Money, State
  • Modelled on: Compound · 2021
  • Visible tests: a settled account earns the index growth; joining a freshly initialised market earns nothing
  • Reward: 50 XP for a complete fix

Briefing

Modelled on Compound's Proposal 062 (September 2021): an upgrade to the contract that distributes COMP rewards contained a bug in how reward indexes were initialised. A single comparison treated a market's freshly initialised index wrongly, so accounts that had never been settled in that market were credited as if they had been accruing since the very beginning — and could claim far more COMP than they had earned. Because changes to the protocol had to pass governance, the fix could not ship immediately.

rewards.js is a reconstruction of that accrual step: each market carries a reward index that only grows, each account remembers the index it was last settled at, and an account that has never been settled has index 0.

Fix accrue so a never-settled account starts from the initial index in every initialised market.

Bug report

BUG-COMP062 · Priority: Critical (over-distribution) · Reported by: protocol team

constants.js: SCALE = 1e6, INITIAL_INDEX = 1e6 — every rewards market starts at INITIAL_INDEX; a market with index 0 has never been initialised.

accrue(market, account) (market = { index }, account = { balance, index, accrued })

  • start = account.index
  • an account with index 0 has never been settled: in an initialised market (market.index >= INITIAL_INDEX) it starts from INITIAL_INDEX instead
  • earned = floor(balance * (market.index - start) / SCALE)
  • then account.index = market.index, account.accrued += earned; return earned

So an account that joins a market the moment its rewards begin earns 0.

Observed: accounts in a market whose index had just been initialised were credited balance * 1 COMP each on their first claim.

Logs

[comptroller] market cXYZ rewards initialised index=1000000
[comptroller] claim 0x9a..: supplierIndex=0 delta=1000000 accrued=4200
[comptroller] claim 0x3c..: supplierIndex=0 delta=1000000 accrued=91000

The code as shipped

src/rewards/rewards.js (editable)

var C = require("./constants");

// Settles an account's reward accrual against the market's current index.
exports.accrue = function (market, account) {
  var start = account.index;
  if (start === 0 && market.index > C.INITIAL_INDEX) {
    start = C.INITIAL_INDEX;
  }
  var earned = Math.floor(account.balance * (market.index - start) / C.SCALE);
  account.index = market.index;
  account.accrued += earned;
  return earned;
};

Read-only context: src/rewards/constants.js.

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