Our synthetic reward pool starts with one participant holding 100 stake units and emits ten reward units per second. At second ten a second participant deposits another 100 units. The first participant earns all emissions before that deposit and half the emissions afterward.

At second twenty the correct totals are 150 reward units for the first participant and 50 for the second. A misplaced early return makes the totals 100 and 100. Both executions distribute 200 units overall so a test checking only the aggregate misses the allocation error.

Pool time advances independently of one account

The accumulator records reward units per stake unit at a chosen precision. Advancing the pool multiplies elapsed seconds by the emission rate and divides the result across the stake that existed during that interval. A participant's debt records which portion of that accumulator their current stake has already accounted for.

Before changing the total stake we settle the interval using the old total. The incoming participant has zero stake during that settlement but the pool still has work to do. Returning early because this participant has nothing to claim skips the update that separates the two ownership periods.

function settle(pool, account, now, broken = false) {
  if (broken && account.stake === 0n) return 0n;
  update(pool, now);
  if (account.stake === 0n) return 0n;
  const accrued = (account.stake * pool.acc) / P;
  const due = accrued - account.debt;
  account.debt = accrued;
  return due;
}

The example uses an independent BigInt model and a precision factor divisible by its chosen stake amounts. Those choices avoid rounding residue so we can isolate the timing error. A Solidity implementation must also account for integer truncation when the ratio is not exact.

A new position must begin at the current accumulator

After the old interval is settled we add the incoming stake and set its debt using the current accumulator. That debt prevents the new position from receiving rewards emitted before it arrived. Setting debt to zero at deposit assigns historical rewards to a participant who did not fund the position during that interval.

The broken branch in the fixture skips the update for an empty participant. It then increases total stake while the pool clock still points to second zero. At second twenty the entire elapsed interval is divided across the new total even though that total existed for only half the interval.

We have to settle the shared clock before changing the denominator because the pool's recorded elapsed interval belongs to the owners of the previous stake distribution and a new account with nothing to claim can still change every existing participant's entitlement when its deposit increases that denominator.

This failure path explains why local checks on one participant are insufficient. A harmless looking return for an empty balance can change the global accounting context used by the next operation.

Conservation and fairness are separate properties

The fixture compares the correct and broken distributions and verifies that both conserve the aggregate reward amount. Conservation catches missing or duplicated emissions but cannot establish that each participant receives the portion earned during their actual ownership period.

We therefore test a historical ownership property. A participant joining later receives no allocation from earlier time. A participant leaving earlier receives no allocation from later time. Testing both directions catches errors that an aggregate conservation assertion accepts.

Correct allocation 150:50; stale clock allocation 100:100
5 assertions passed

For an empty pool the implementation needs a different explicit policy. It can advance the clock without allocating rewards or carry the interval forward under a defined distribution rule. Leaving the clock untouched accidentally is not a policy because the next depositor then receives behavior determined by call order.

Carry the model into contract sequences

In Solidity tests we exercise deposit and withdrawal alongside claims. Transferable stake adds another transition that must settle the relevant positions before ownership changes. Reward token replacement introduces separate accounting periods and cannot reuse debt from a different token without a conversion rule.

We keep external reward transfers after the accounting changes and test a recipient that reenters an accessible function. The pure fixture models neither callbacks nor transaction rollback. Its job is to define the expected allocation so an EVM test can compare the actual implementation against a small reference calculation.

An empty pool interval adds a policy question that the two participant example leaves outside its boundary. State who owns those emissions before depositing again and verify every later claim against that ownership rule.

Sources and examples